F-Algebren und F-Kohlegebren sind mathematische Strukturen, die beim Denken über induktive Typen (oder rekursive Typen ) eine wichtige Rolle spielen .
F-Algebren
Wir beginnen zuerst mit F-Algebren. Ich werde versuchen, so einfach wie möglich zu sein.
Ich denke, Sie wissen, was ein rekursiver Typ ist. Dies ist beispielsweise ein Typ für eine Liste von Ganzzahlen:
data IntList = Nil | Cons (Int, IntList)
Es ist offensichtlich, dass es rekursiv ist - tatsächlich bezieht sich seine Definition auf sich selbst. Seine Definition besteht aus zwei Datenkonstruktoren, die die folgenden Typen haben:
Nil :: () -> IntList
Cons :: (Int, IntList) -> IntList
Beachten Sie, dass ich Art geschrieben von Nilals () -> IntListnicht einfach IntList. Dies sind in der Tat aus theoretischer Sicht äquivalente Typen, da der ()Typ nur einen Einwohner hat.
Wenn wir Signaturen dieser Funktionen satztheoretischer schreiben, erhalten wir
Nil :: 1 -> IntList
Cons :: Int × IntList -> IntList
Dabei 1handelt es sich um eine Einheitensatz (Satz mit einem Element) und die A × BOperation ist ein Kreuzprodukt aus zwei Sätzen Aund B(dh einem Satz von Paaren, bei (a, b)denen aalle Elemente von Aund balle Elemente von durchlaufen werden B).
Disjunkte Vereinigung von zwei Mengen Aund Bist eine Menge, A | Bdie eine Vereinigung von Mengen {(a, 1) : a in A}und ist {(b, 2) : b in B}. Im Wesentlichen handelt es sich um eine Menge aller Elemente aus beiden Aund B, wobei jedoch jedes dieser Elemente als zu einem Aoder gekennzeichnet Bist. Wenn wir also ein Element aus auswählen, A | Bwissen wir sofort, ob dieses Element von Aoder von stammt B.
Wir können 'verbinden' Nilund ConsFunktionen, so dass sie eine einzige Funktion bilden, die an einer Menge arbeitet 1 | (Int × IntList):
Nil|Cons :: 1 | (Int × IntList) -> IntList
Wenn die Nil|ConsFunktion auf den ()Wert angewendet wird (der offensichtlich zur 1 | (Int × IntList)Menge gehört), verhält sie sich tatsächlich so, als ob sie es wäre Nil. Wenn Nil|Conses auf einen Wert vom Typ angewendet wird (Int, IntList)(solche Werte sind auch in der Menge enthalten 1 | (Int × IntList), verhält es sich wie folgt) Cons.
Betrachten Sie nun einen anderen Datentyp:
data IntTree = Leaf Int | Branch (IntTree, IntTree)
Es hat die folgenden Konstruktoren:
Leaf :: Int -> IntTree
Branch :: (IntTree, IntTree) -> IntTree
die auch zu einer Funktion verbunden werden kann:
Leaf|Branch :: Int | (IntTree × IntTree) -> IntTree
Es ist ersichtlich, dass beide joinedFunktionen einen ähnlichen Typ haben: Sie sehen beide so aus
f :: F T -> T
Wo Fist eine Art von Transformation, die unseren Typ nimmt und einen komplexeren Typ ergibt, der aus xund |Operationen, Verwendungen Tund möglicherweise anderen Typen besteht. Zum Beispiel für IntListund IntTree Fsieht wie folgt aus:
F1 T = 1 | (Int × T)
F2 T = Int | (T × T)
Wir können sofort feststellen, dass jeder algebraische Typ auf diese Weise geschrieben werden kann. In der Tat werden sie deshalb als "algebraisch" bezeichnet: Sie bestehen aus einer Reihe von "Summen" (Gewerkschaften) und "Produkten" (Kreuzprodukten) anderer Typen.
Jetzt können wir die F-Algebra definieren. F-Algebra ist nur ein Paar (T, f), wobei Tes sich um einen Typ handelt und feine Funktion des Typs ist f :: F T -> T. In unseren Beispielen sind F-Algebren (IntList, Nil|Cons)und (IntTree, Leaf|Branch). Beachten Sie jedoch, dass trotz dieser Art von fFunktion für jedes F die gleiche ist Tund fselbst beliebig sein kann. Zum Beispiel (String, g :: 1 | (Int x String) -> String)oder (Double, h :: Int | (Double, Double) -> Double)für einige gund hsind auch F-Algebren für entsprechende F.
Anschließend können wir F-Algebra-Homomorphismen und dann anfängliche F-Algebren einführen , die sehr nützliche Eigenschaften haben. In der Tat (IntList, Nil|Cons)ist eine anfängliche F1-Algebra und (IntTree, Leaf|Branch)ist eine anfängliche F2-Algebra. Ich werde keine genauen Definitionen dieser Begriffe und Eigenschaften präsentieren, da sie komplexer und abstrakter als nötig sind.
Die Tatsache, dass es sich beispielsweise (IntList, Nil|Cons)um eine F-Algebra handelt, ermöglicht es uns jedoch, eine foldähnliche Funktion für diesen Typ zu definieren . Wie Sie wissen, ist fold eine Art Operation, die einen rekursiven Datentyp in einen endlichen Wert umwandelt. Zum Beispiel können wir eine Liste von Ganzzahlen in einen einzelnen Wert falten, der eine Summe aller Elemente in der Liste ist:
foldr (+) 0 [1, 2, 3, 4] -> 1 + 2 + 3 + 4 = 10
Es ist möglich, eine solche Operation auf einen beliebigen rekursiven Datentyp zu verallgemeinern.
Das Folgende ist eine Signatur der foldrFunktion:
foldr :: ((a -> b -> b), b) -> [a] -> b
Beachten Sie, dass ich geschweifte Klammern verwendet habe, um die ersten beiden Argumente vom letzten zu trennen. Dies ist keine echte foldrFunktion, aber sie ist isomorph dazu (das heißt, Sie können leicht eine von der anderen erhalten und umgekehrt). Teilweise angewendet foldrhat die folgende Unterschrift:
foldr ((+), 0) :: [Int] -> Int
Wir können sehen, dass dies eine Funktion ist, die eine Liste von Ganzzahlen verwendet und eine einzelne Ganzzahl zurückgibt. Definieren wir eine solche Funktion anhand unseres IntListTyps.
sumFold :: IntList -> Int
sumFold Nil = 0
sumFold (Cons x xs) = x + sumFold xs
Wir sehen, dass diese Funktion aus zwei Teilen besteht: Der erste Teil definiert das Verhalten dieser Funktion zum NilTeil IntListund der zweite Teil definiert das Verhalten der Funktion zum ConsTeil.
Nehmen wir nun an, wir programmieren nicht in Haskell, sondern in einer Sprache, die die Verwendung algebraischer Typen direkt in Typensignaturen ermöglicht (technisch gesehen erlaubt Haskell die Verwendung algebraischer Typen über Tupel und Either a bDatentypen, dies führt jedoch zu unnötiger Ausführlichkeit). Betrachten Sie eine Funktion:
reductor :: () | (Int × Int) -> Int
reductor () = 0
reductor (x, s) = x + s
Es ist zu sehen, dass dies reductoreine Funktion des Typs ist F1 Int -> Int, genau wie bei der Definition der F-Algebra! In der Tat ist das Paar (Int, reductor)eine F1-Algebra.
Da IntListein anfänglicher F1-Algebra, für jeden Typ Tund für jede Funktion r :: F1 T -> Tes eine Funktion gibt, genannt catamorphism für r, das umwandelt IntListauf T, und eine solche Funktion ist einzigartig. Denn in unserem Beispiel ein catamorphism für reductorist sumFold. Beachten Sie, wie reductorund sumFoldähnlich sind: Sie haben fast die gleiche Struktur! In der reductorDefinition entspricht die Verwendung von sParametern (deren Typ entspricht T) der Verwendung des Ergebnisses der Berechnung von sumFold xsin sumFoldDefinition.
Um es klarer zu machen und Ihnen zu helfen, das Muster zu sehen, hier ein weiteres Beispiel, und wir beginnen erneut mit der resultierenden Faltfunktion. Betrachten Sie die appendFunktion, die ihr erstes Argument an das zweite anfügt:
(append [4, 5, 6]) [1, 2, 3] = (foldr (:) [4, 5, 6]) [1, 2, 3] -> [1, 2, 3, 4, 5, 6]
So sieht es auf unserer IntList:
appendFold :: IntList -> IntList -> IntList
appendFold ys () = ys
appendFold ys (Cons x xs) = x : appendFold ys xs
Versuchen wir noch einmal, den Reduktor aufzuschreiben:
appendReductor :: IntList -> () | (Int × IntList) -> IntList
appendReductor ys () = ys
appendReductor ys (x, rs) = x : rs
appendFoldist ein catamorphism für appendReductorwelche transformiert IntListin IntList.
Mit F-Algebren können wir also im Wesentlichen 'Falten' für rekursive Datenstrukturen definieren, dh Operationen, die unsere Strukturen auf einen gewissen Wert reduzieren.
F-Kohlegebren
F-Kohlegebren sind sogenannte "duale" Begriffe für F-Algebren. Sie ermöglichen es uns, unfoldsfür rekursive Datentypen eine Möglichkeit zu definieren, rekursive Strukturen aus einem bestimmten Wert zu konstruieren.
Angenommen, Sie haben den folgenden Typ:
data IntStream = Cons (Int, IntStream)
Dies ist ein unendlicher Strom von ganzen Zahlen. Sein einziger Konstruktor hat den folgenden Typ:
Cons :: (Int, IntStream) -> IntStream
Oder in Form von Sets
Cons :: Int × IntStream -> IntStream
Mit Haskell können Sie Musterübereinstimmungen für Datenkonstruktoren erstellen, sodass Sie die folgenden Funktionen definieren können, die an IntStreams arbeiten:
head :: IntStream -> Int
head (Cons (x, xs)) = x
tail :: IntStream -> IntStream
tail (Cons (x, xs)) = xs
Sie können diese Funktionen natürlich zu einer einzigen Funktion des Typs zusammenfügen IntStream -> Int × IntStream:
head&tail :: IntStream -> Int × IntStream
head&tail (Cons (x, xs)) = (x, xs)
Beachten Sie, wie das Ergebnis der Funktion mit der algebraischen Darstellung unseres IntStreamTyps übereinstimmt . Ähnliches kann auch für andere rekursive Datentypen durchgeführt werden. Vielleicht haben Sie das Muster bereits bemerkt. Ich beziehe mich auf eine Familie von Funktionen des Typs
g :: T -> F T
Wo Tist ein Typ? Von nun an werden wir definieren
F1 T = Int × T
Nun ist F-Kohlegebra ein Paar (T, g), wobei Tes sich um einen Typ und geine Funktion des Typs handelt g :: T -> F T. Zum Beispiel (IntStream, head&tail)ist eine F1-Kohlegebra. Auch hier, wie in F-Algebren, gund Twillkürlich kann zum Beispiel (String, h :: String -> Int x String)auch ein F1-Koalgebra für einige Stunden.
Unter allen F-Kohlegebren gibt es sogenannte terminale F-Kohlegebren , die zu anfänglichen F-Algebren dual sind. Zum Beispiel IntStreamist eine terminale F-Kohlegebra. Dies bedeutet , dass für jeden Typ Tund für jede Funktion p :: T -> F1 Tes eine Funktion gibt, genannt Anamorphismus , das umwandelt Tauf IntStream, und eine solche Funktion ist einzigartig.
Betrachten Sie die folgende Funktion, die ausgehend von der angegebenen einen Strom aufeinanderfolgender Ganzzahlen generiert:
nats :: Int -> IntStream
nats n = Cons (n, nats (n+1))
Lassen Sie uns nun eine Funktion untersuchen natsBuilder :: Int -> F1 Int, dh natsBuilder :: Int -> Int × Int:
natsBuilder :: Int -> Int × Int
natsBuilder n = (n, n+1)
Wieder können wir eine gewisse Ähnlichkeit zwischen natsund sehen natsBuilder. Es ist sehr ähnlich zu der Verbindung, die wir zuvor mit Reduktoren und Falten beobachtet haben. natsist ein Anamorphismus für natsBuilder.
Ein weiteres Beispiel ist eine Funktion, die einen Wert und eine Funktion annimmt und einen Strom aufeinanderfolgender Anwendungen der Funktion auf den Wert zurückgibt:
iterate :: (Int -> Int) -> Int -> IntStream
iterate f n = Cons (n, iterate f (f n))
Die Builder-Funktion ist die folgende:
iterateBuilder :: (Int -> Int) -> Int -> Int × Int
iterateBuilder f n = (n, f n)
Dann iterateist ein Anamorphismus für iterateBuilder.
Fazit
Kurz gesagt, F-Algebren ermöglichen es, Falten zu definieren, dh Operationen, die die rekursive Struktur auf einen einzigen Wert reduzieren, und F-Kohlegebren ermöglichen das Gegenteil: Konstruieren Sie eine [potenziell] unendliche Struktur aus einem einzigen Wert.
Tatsächlich fallen in Haskell F-Algebren und F-Kohlegebren zusammen. Dies ist eine sehr schöne Eigenschaft, die eine Folge des Vorhandenseins eines "unteren" Werts in jedem Typ ist. In Haskell können also für jeden rekursiven Typ sowohl Falten als auch Entfaltungen erstellt werden. Das theoretische Modell dahinter ist jedoch komplexer als das oben vorgestellte, weshalb ich es bewusst vermieden habe.
Hoffe das hilft.