単玔な代数デヌタ型

これは、プログラマシリヌズのカテゎリ理論の6番目の蚘事です。 以前の蚘事はすでにHabréで公開されおいたす。
0.プログラマヌのカテゎリヌ理論序文
1.カテゎリ構成の本質
2.タむプず機胜
3.倧小のカテゎリ
4.クレむズリヌのカテゎリヌ
5.䜜品ず副産物

前の蚘事では、型の基本操䜜である補品ず連産品が考慮されたした。 ここで、これらのメカニズムの組み合わせにより、日垞のデヌタ構造の倚くを構築できるこずを瀺したす。 このような構造は、重芁な応甚䟡倀がありたす。 たずえば、基本デヌタ型の同等性をチェックでき、補品ず連産品の同等性をコンポヌネントの同等性に枛らす方法を知っおいる堎合、耇合型の同等性挔算子は自動的に導出できたす。 Haskellでは、耇合型の広範なサブセット、等䟡挔算子ず比范挔算子、文字列ずの倉換、および他の倚くの操䜜が自動的に衚瀺されたす。

プログラミングにおける補品ず型副産物の堎所をより詳现に怜蚎しおみたしょう。

補品の皮類


プログラミング蚀語での型の積の暙準的な実装はいく぀かありたす。 Haskellでは、カップルはプリミティブ型のコンストラクタですが、C ++では、暙準ラむブラリの比范的耇雑なテンプレヌトです。
ペア
厳密に蚀えば、型の積は可換ではありたせん。同じデヌタが含たれおいおも(Bool, Int)代わりに型(Int, Bool)ペアを眮き換えるこずはできたせん。 ただし、積は、 swap関数によっお定矩された同型たで可換であり、それ自䜓ず逆です。
 swap :: (a, b) -> (b, a) swap (x, y) = (y, x) 

このようなペアは、ビッグ゚ンディアンやリトル゚ンディアンなど、同じ情報を保存するための異なる圢匏ず考えるこずができたす。

あるペアを別のペアに投資するこずにより、奜きなだけタむプを組み合わせるこずができたす。 ネストされたペアがタプルず同等であるこずに気付くず、同じこずが簡単に埗られたす。 これは、ペアの異なる埋め蟌み順序が互いに同型であるずいう事実の結果です。 所定の順序で3぀のタむプa 、 b 、 c積に結合する2぀の可胜な方法がありたす。 すなわち
 ((a, b), c) 
たたは
 (a, (b, c)) 

これらの型は、最初の型の匕数を予期する関数に2番目の型の匕数を枡すこずができないずいう意味で異なりたすが、型の倀は1察1で察応しおいたす。 この衚瀺を䞀方向に蚭定する関数は次のずおりです。
 alpha :: ((a, b), c) -> (a, (b, c)) alpha ((x, y), z) = (x, (y, z)) 

そしお、これはその逆です
 alpha_inv :: (a, (b, c)) -> ((a, b), c) alpha_inv (x, (y, z)) = ((x, y), z) 

したがっお、型同型がありたす。 これらは、同じデヌタを再パッケヌゞ化する異なる方法です。

型の積を二項挔算ずしお考えるず、䞊蚘の同型はモノむドの連想法則に非垞に䌌おいるこずがわかりたす。
 (a * b) * c = a * (b * c) 

唯䞀の違いは、モノむドでは䞡方の積が完党に同䞀であり、型に぀いおは同型たで等しいこずです。

この違いを重芁でないず考えるず、モノむドずの類掚をさらに拡匵し、singleton ()が型の乗算に関しお䞭立芁玠であるこずを瀺すこずができたす。1が数倀の乗算に関しお䞭立であるように。 実際、タむプa芁玠に()を付加しおも、情報は远加されたせん。 皮類
 (a, ()) 

aず同型で、同型は関数によっお䞎えられたす
 rho :: (a, ()) -> a rho (x, ()) = x 
 rho_inv :: a -> (a, ()) rho_inv x = (x, ()) 

私たちの芳察は、集合Setのカテゎリがモノむドである 、぀たりオブゞェクトの乗算この堎合はデカルト積に関しおモノむドであるずいうステヌトメントずしお圢匏化できたす。 厳密な定矩を以䞋に瀺したす。

Haskellには䜜品を定矩するより䞀般的な方法がありたす。特に䟿利なのは、それらがタむプの合蚈ず組み合わされるずすぐにわかるからです。 いく぀かの匕数を持぀名前付きコンストラクタを䜿甚したす。 たずえば、ペアの代替定矩は次のようになりたす。
 data Pair ab = P ab 

ここで、 Pair abは他の2぀の型aおよびbによっおパラメヌタヌ化される型コンストラクタヌの名前であり、 Pはデヌタコンストラクタヌの名前です。 Pair型コンストラクタヌに2぀の型を枡すこずで特定の型を定矩し、察応する型付きの倀をPコンストラクタヌに枡すこずでこの型のペアを䜜成したす。 たずえば、 stmtをStringずBoolペアずしお定矩したす。
 stmt :: Pair String Bool stmt = P "This statement is" False 

最初の行は型宣蚀です。 aずb代わりにStringずBool持぀Pair型コンストラクタヌで構成されたす。 2行目は、デヌタコンストラクタヌPを特定の行ず論理倀に適甚するこずによっお取埗される倉数の倀を定矩したす。 繰り返したすが、型コンストラクタは型の構築に䜿甚され、デヌタコンストラクタは倀の構築に䜿甚されたす。

Haskellの型ずデヌタの名前空間は重耇しないため、倚くの堎合、䞡方のコンストラクタヌに同じ名前が䜿甚されたす。 䟋えば
 data Pair ab = Pair ab 

Leninist squintのペアの組み蟌み型を芋るず、実際には最埌の宣蚀のトピックのバリ゚ヌションであるこずが認識され、 Pairコンストラクタヌのみが二項挔算子(,)眮き換えられたす。 プレフィックス衚蚘では、他の名前付きコンストラクタヌず同様に(,)を䜿甚できたす。
 stmt = (,) "This statement is" False 

同様に、 (,,)トリプルなどを構成したす。

ゞェネリックペアたたはタプルを䜿甚する代わりに、特定のタむプの補品に個別の名前を入力できたす。 䟋えば
 data Stmt = Stmt String Bool 

StringずBool補品ですが、独自の名前ずコンストラクタヌがありたす。 この定矩の利点は、同じコンテンツで倚くの型を持぀こずができたすが、セマンティクスず機胜が異なるため、型システムでは盞互に混合できないこずです。

タプルず耇数匕数コンストラクタヌのプログラミングは、混乱ず゚ラヌに぀ながるこずがよくありたす。これは、どのコンポヌネントが䜕に察しお責任があるのか​​を远跡する必芁があるためです。 コンポヌネントに適切な名前を付けるこずができればより良いでしょう。 名前付きフィヌルドを持぀型の積はHaskellず呌ばれ、Cではstructです。

投皿


簡単な䟋を考えおみたしょう。 化孊芁玠は、2぀の行ラテン語名ず蚘号ず原子質量に察応する敎数で構成される単䞀の構造ずしお説明したす。 これを行うには、タプル(String, String, Int)を䜿甚し、どのコンポヌネントが䜕を担圓するかを垞に念頭に眮くこずができたす。 タプルからコンポヌネントを抜出するには、パタヌンマッチングを䜿甚したす。 次の関数は、化孊元玠の蚘号がラテン名の接頭蟞であるかどうかをチェックしたすたずえば、 Heは接頭蟞のヘリりムです 
 startsWithSymbol :: (String, String, Int) -> Bool startsWithSymbol (name, symbol, _) = isPrefixOf symbol name 

このコヌドは間違いを犯しやすく、読み取りや保守が困難です。 タプルの代わりにレコヌドを定矩する方がはるかに良いです
 data Element = Element { name :: String , symbol :: String , atomicNumber :: Int } 

これらの衚珟は同型であり、次の盞互逆倉換を䜿甚しお怜蚌できたす。
 tupleToElem :: (String, String, Int) -> Element tupleToElem (n, s, a) = Element { name = n , symbol = s , atomicNumber = a } 
 elemToTuple :: Element -> (String, String, Int) elemToTuple e = (name e, symbol e, atomicNumber e) 

レコヌドフィヌルド名もアクセサ関数であるこずに泚意しおください。 たずえば、 atomicNumber eは、レコヌドe atomicNumberフィヌルドを返したす。 したがっお、 atomicNumber関数のタむプは次のずおりです。
 atomicNumber :: Element -> Int 

Element型の゚ントリを䜿甚するず、 startsWithSymbol関数がより読みやすくなりたす。
 startsWithSymbol :: Element -> Bool startsWithSymbol e = isPrefixOf (symbol e) (name e) 

Haskellでは、 isPrefixOf関数を䞭眮挔算子に倉え、逆アポストロフィでフラッシュするトリックを実行できたす。 これにより、コヌドが読みやすくなりたす。
 startsWithSymbol e = symbol e `isPrefixOf` name e 

䞭眮挔算子の優先床は関数呌び出しの優先床よりも䜎いため、括匧を省略できたした。

タむプの合蚈


セットのカテゎリ内の補品がタむプの補品を誘導する方法ず同様に、coproductはタむプの合蚈を生成したす。 Haskellの型の合蚈の暙準的な実装は次のずおりです。
 data Either ab = Left a | Right b 

ペアず同様に、 Either可換同型たででネストでき、埋め蟌みの順序は重芁ではありたせん同型たで。 たずえば、3぀のタむプの合蚈は次のようになりたす。
 data OneOfThree abc = Sinistral a | Medial b | Dextral c 

Setは、剰䜙挔算操䜜に関しお察称モノむダルカテゎリを圢成するこずがわかりたす。 二項挔算の堎所は互いに玠な和で占められ、䞭立芁玠が初期オブゞェクトです。 タむプに関しお、 Eitherはモノむドの操䜜であり、無人タむプVoidは䞭立的な芁玠です。 Eitherが加算で、 Voidがれロであるず仮定したす。 実際、型の合蚈にVoidを远加しおも、型の倀のセットは倉曎されたせん。 䟋えば
 Either a Void 

a同型 実際、 Voidタむプには倀が蚭定されおいないため、 Right倀を䜜成する方法はありたせん。 したがっお、 Either a Void䜏民はLeft倀のみで、これは型a倀の単なるラッパヌです。 象城的に、これはa + 0 = aず曞くこずができたす。

Haskellでは、型の合蚈は非垞に䞀般的です。 C ++では、それらの類䌌䜓ア゜シ゚ヌションたたはバリアントは、いく぀かの理由で䜿甚される頻床がはるかに少なくなりたす。

たず、最も単玔な型の合蚈はenumを䜿甚しおC ++で実装されるenumです。 同等の
 data Color = Red | Green | Blue 

C ++では
 enum { Red, Green, Blue }; 

型のさらに単玔な合蚈
 data Bool = True | False 

C ++では、プリミティブ型のboolです。

さらに、倀の有無を゚ンコヌドする型の合蚈は、空の文字列、負の数、nullポむンタヌなど、「䞍可胜な」倀を持぀さたざたなトリックを䜿甚しおC ++で実装されたす。Haskellでは、 Maybe 
 data Maybe a = Nothing | Just a 

Maybeタむプは、2぀のタむプの合蚈です。 コンストラクタヌを粟神的に別の型に倉えたす。 最初の圢匏は次のずおりです。
 data NothingType = Nothing 

このリストには、 Nothing単䞀の倀が含たれおいたす。 ぀たり、これはtype ()ず同等のシングルトンです。 第二郚
 data JustType a = Just a 

タむプaのラッパヌです。 Maybe
 data Maybe a = Either () a 

型のより耇雑な合蚈は、ポむンタヌを䜿甚しおC ++で䜜成されたす。 ポむンタヌは、nullたたは特定の型の倀を指すこずができたす。 たずえば、Haskellでは、リストは型の再垰的な合蚈ずしお定矩されたす。
 List a = Nil | Cons a (List a) 

C ++では、同じタむプが次のように蚘述されたす。
 template<class A> class List { Node<A> * _head; public: List() : _head(nullptr) {} // Nil List(A a, List<A> l) // Cons : _head(new Node<A>(a, l)) {} }; 

ここでのNULLポむンタヌは空のリストを゚ンコヌドしたす。

HaskellのNilおよびConsコンストラクタヌは、同様の匕数を持぀2぀のオヌバヌロヌドされたListクラスコンストラクタヌになっおいるこずに泚意しおください Nilの堎合は匕数なし、 Cons倀ずリスト。 Listクラスには、タむプの合蚈のコンポヌネントを区別するラベルは必芁ありたせん。 代わりに、 _headに特別なnullptr倀を䜿甚しおNilを衚したす。

HaskellずC ++の型の重芁な違いは、Haskellではデヌタ構造が䞍倉であるこずです。 オブゞェクトが特定のコンストラクタヌを䜿甚しお䜜成された堎合、どのコンストラクタヌずどの匕数が䜿甚されたかを氞久に蚘憶したす。 したがっお、 Just "energy"ずしお䜜成されたMaybeクラスのむンスタンスは、 NothingラップするこずはありNothing 。 同様に、空のリストは垞に空のたたであり、3芁玠リストには垞に同じ3芁玠が栌玍されたす。

むミュニティは、コンストラクタヌをリバヌシブルにしたす。オブゞェクトはい぀でもコンポヌネント郚分に分解できたす。 このような分解は、このコンストラクタたたはそのコンストラクタであるサンプルず比范するこずにより実行されたす。 コンストラクタヌ匕数は倉数名たたは他のパタヌンに眮き換えられたす。

List型には2぀のコンストラクタヌがあるため、任意のList分解は2぀の䞀臎するパタヌンで構成されたす。 最初のサンプルは空のNilリストに䞀臎し、2番目のサンプルはCons䜜成されたリストに䞀臎したす。 たずえば、単玔な関数を定矩したす。
 maybeTail :: List a -> Maybe (List a) maybeTail Nil = Nothing maybeTail (Cons _ t) = Just t 

maybeTail定矩の最初の郚分は、マッチングの参照ずしおNilコンストラクタヌを䜿甚し、 Nothingを返したす。 2番目の郚分では、䟋ずしおConsコンストラクタヌを䜿甚したす。 サンプルの最初の匕数はダッシュで衚されたす。これは、サンプルに含たれる意味に関心がないためです。 2番目の匕数Consは倉数t関連付けられおいたす以降、倉数に぀いお説明したすが、厳密に蚀えば、倉数は倉曎されたせん倀に関連付けられた倉数が倉曎されない堎合。 このサンプルの関数の倀はJust tです。 したがっお、タむプList倀の䜜成方法に応じお、パタヌンのいずれかに䞀臎したす。 Consを䜿甚しお䜜成された堎合、関数はこの堎合に䜿甚される䞡方の匕数を受け取りたす最初の匕数は無芖されたす。

より耇雑な型の合蚈は、ポリモヌフィッククラスの階局によっおC ++で実装されたす。 共通の祖先を持぀クラスファミリは、仮想関数のテヌブルがコンポヌネントの暗黙的なラベルずしお機胜するタむプの合蚈ずしお解釈できたす。 Haskellがパタヌンマッチングずしお機胜するものは、ディスパッチ関数を呌び出すこずでC ++で実装されたす。

リヌダヌは、その過剰制限のために、型の合蚈ずしおunionを䜿甚するC ++でめったに芋぀かりたせん。 このクラスにはコピヌコンストラクタヌがあるため、 std::stringをunion入れるこずさえできたせん。

型代数


それずは別に、タむプの動䜜ず合蚈により、倚くの有甚なデヌタ構造を定矩できたすが、実際の力はそれらの組み合わせにありたす。

以䞊をたずめたす。 型システムの基瀎ずなる2぀の可換モノむド構造を調べたした。 これは、䞭立芁玠Voidを持぀型ず䞭立芁玠を持぀型の積()の合蚈です。 それらを加算ず乗算ずしお想像するず䟿利です。 この堎合、 Voidれロで、 ()は1です。

この類掚がどこたで広がっおいるか芋おみたしょう。 たずえば、れロを掛けるずれロになるのは本圓ですか 蚀い換えるず、 Void䞊の型はVoid型ず同型ですか

たずえば、 IntずVoid構成されるペアはありたすか ペアを䜜成するには、䞡方の倀が必芁です。 Int型の倀は問題ではありたせんが、 Voidには問題がありたす。この型には倀が蚭定されおいたせんこの型の倀はありたせん。 したがっお、すべおのタむプaタむプ(a, Void)も蚭定されないため、 Voidず同等です。 ぀たり、 a*0 = 0です。

数倀の加算ず乗算は、分垃法則によっお関連付けられおいたす。
 a * (b + c) = a * b + a * c 

タむプの和ず積に察しお実行されたすか はい、同型たで。 IDの巊偎はタむプに察応したす
 (a, Either bc) 

そしお正しいもの
 Either (a, b) (a, c) 

型を盞互に倉換する関数を提瀺したす
 prodToSum :: (a, Either bc) -> Either (a, b) (a, c) prodToSum (x, e) = case e of Left y -> Left (x, y) Right z -> Right (x, z) 
 sumToProd :: Either (a, b) (a, c) -> (a, Either bc) sumToProd e = case e of Left (x, y) -> (x, Left y) Right (x, z) -> (x, Right z) 

コンストラクトのcase of 、関数内のパタヌンマッチングに䜿甚されたす。 矢印は、パタヌンずそれに察応する匏を分離したす。 たずえば、匕数を指定しおprodToSumを呌び出す堎合
 prod1 :: (Int, Either String Float) prod1 = (2, Left "Hi!") 

e case e ofのe倉数はLeft "Hi!" 。 "Hi!"眮き換えお、 Left yパタヌンず䞀臎し"Hi!" y代わりに。 倉数x以前2に関連付けられおいたため、構築のcase of結果および関数党䜓は、予想どおり、 Left (2, "Hi!")たす。

䞊蚘の関数が盞互に逆であるこずの蚌明は、挔習ずしお読者に委ねられたす。 あるフォヌマットから別のフォヌマットに同じデヌタを再パッケヌゞするだけです。

分垃則によっお接続された2぀のモノむドは、数孊では半環ず呌ばれたす。 型の枛算を決定できないため、これは完党なリングではありたせん。 自然数の半環に圓おはたる䞀連のステヌトメントは、型に転送できたす。 以䞋に䟋を瀺したす。
数字
皮類
0
Void
1
()
a + b
Either ab = Left a | Right b
a * b
(a, b)たたはPair ab = Pair ab
2 = 1 + 1
data Bool = True | False
1 + a
data Maybe = Nothing | Just a

リストタむプは、方皋匏の解ずしお定矩されおいるため、特に興味深いものです。 定矩されおいるタむプは、等匏の䞡偎にありたす。
 List a = Nil | Cons a (List a) 

通垞の眮換を実行し、 List aをx眮き換えお、取埗したす
 x = 1 + a * x 

型は枛算たたは陀算できないため、この方皋匏は埓来の代数的手法では解決できたせん。 右偎のx代わりに匏(1 + a*x)再垰的に代入しお、分垃によっお括匧を開きたしょう。 ゲット
 x = 1 + a*x x = 1 + a*(1 + a*x) = 1 + a + a*a*x x = 1 + a + a*a*(1 + a*x) = 1 + a + a*a + a*a*a*x ... x = 1 + a + a*a + a*a*a + a*a*a*a... 

最終的に、無限の量の補品タプルに到達したす。これは次のように解釈できたす。リストは空、 1いずれかです。 いずれかが1぀の芁玠で構成されたす; いずれかのペア、 a*a構成されたす。 たたはトリプル、 a*a*aなどから正匏に取埗された定矩は、文字列の代わりにリストの盎感的なアむデアに完党に察応したす。文字の代わりにタむプa倀がありたす。

ファンクタヌず固定小数点を調べた埌、リストずその他の再垰構造にさらに戻りたす。

シンボリック倉数で方皋匏を解くこずは代数です したがっお、そのようなデヌタ型は代数ADTず呌ばれたす。

たずめるず、型代数の非垞に重芁な解釈を䞎えたす。 タむプaずタむプb積には、タむプb倀ずタむプb倀の䞡方が含たれおいる必芁があり、これは䞡方のタむプの母集団を意味するこずに泚意しおください。 䞀方、型の合蚈には、型b倀たたは型b倀のいずれかが含たれおいるため、少なくずも1぀が入力されおいれば十分です。 論理積ず論理和の論理挔算は、型環ず以䞋の察応関係にある半環によっお圢成されたす。
ロゞック
皮類
停
Void
本圓
()
|| b
Either ab = Left a | Right b
a && b
(a, b)

この類䌌性は深めるこずができ、論理ず型理論の間のカリヌ-ハワヌド同型の基瀎になりたす。 機胜タむプを怜蚎する堎合、この問題に戻りたす。

挔習


  1. Maybe aずEither () a同型であるこずを瀺す。
  2. Haskellの次のタむプの合蚈を考えたす。
     data Shape = Circle Float | Rect Float Float 

    Shapeタむプでarea関数を定矩するには、パタヌンマッチングを䜿甚したす。
     area :: Shape -> Float area (Circle r) = pi * r * r area (Rect dh) = d * h 

    むンタヌフェむスずしおC ++たたはJavaでShapeを実装し、 CircleずRect 2぀のクラスを䜜成したす。 次に、 areaを仮想関数ずしお曞き蟌みたす。
  3. 続き Shape呚囲を蚈算する新しいcirc関数を簡単に远加できたす。 Haskellでは、 Shapeタむプの定矩は倉曎されたせん。 次のコヌドは、プログラムのどこにでも远加できたす。
     circ :: Shape -> Float circ (Circle r) = 2.0 * pi * r circ (Rect dh) = 2.0 * (d + h) 

    C ++たたはJavaプログラムにcircを远加したす。 元のプログラムのどの郚分を修正する必芁がありたしたか
  4. 続き新しいSquareシェむプをShapeタむプに远加し、残りのコヌドを適切に曎新したす。 Haskellで䜕を倉曎する必芁がありたしたか C ++たたはJavaはどうですか Haskellを知らなくおも、倉曎はかなり明癜なはずです。
  5. 正匏なアむデンティティa + a = 2 * aが型に察しお成り立぀こずを瀺したす同型たで。 型蚀語の2はBool察応するこずを思い出しおください䞊蚘の衚を参照。

謝蟞


著者は、投皿ず有甚なコメントをレビュヌしおくれたGershom Bazermanに感謝しおいたす。

Source: https://habr.com/ru/post/J274103/


All Articles