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 Nil
als () -> IntList
nicht 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 1
handelt es sich um eine Einheitensatz (Satz mit einem Element) und die A × B
Operation ist ein Kreuzprodukt aus zwei Sätzen A
und B
(dh einem Satz von Paaren, bei (a, b)
denen a
alle Elemente von A
und b
alle Elemente von durchlaufen werden B
).
Disjunkte Vereinigung von zwei Mengen A
und B
ist eine Menge, A | B
die 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 A
und B
, wobei jedoch jedes dieser Elemente als zu einem A
oder gekennzeichnet B
ist. Wenn wir also ein Element aus auswählen, A | B
wissen wir sofort, ob dieses Element von A
oder von stammt B
.
Wir können 'verbinden' Nil
und Cons
Funktionen, so dass sie eine einzige Funktion bilden, die an einer Menge arbeitet 1 | (Int × IntList)
:
Nil|Cons :: 1 | (Int × IntList) -> IntList
Wenn die Nil|Cons
Funktion 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|Cons
es 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 joined
Funktionen einen ähnlichen Typ haben: Sie sehen beide so aus
f :: F T -> T
Wo F
ist eine Art von Transformation, die unseren Typ nimmt und einen komplexeren Typ ergibt, der aus x
und |
Operationen, Verwendungen T
und möglicherweise anderen Typen besteht. Zum Beispiel für IntList
und IntTree
F
sieht 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 T
es sich um einen Typ handelt und f
eine 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 f
Funktion für jedes F die gleiche ist T
und f
selbst beliebig sein kann. Zum Beispiel (String, g :: 1 | (Int x String) -> String)
oder (Double, h :: Int | (Double, Double) -> Double)
für einige g
und h
sind 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 foldr
Funktion:
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 foldr
Funktion, aber sie ist isomorph dazu (das heißt, Sie können leicht eine von der anderen erhalten und umgekehrt). Teilweise angewendet foldr
hat 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 IntList
Typs.
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 Nil
Teil IntList
und der zweite Teil definiert das Verhalten der Funktion zum Cons
Teil.
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 b
Datentypen, 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 reductor
eine 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 IntList
ein anfänglicher F1-Algebra, für jeden Typ T
und für jede Funktion r :: F1 T -> T
es eine Funktion gibt, genannt catamorphism für r
, das umwandelt IntList
auf T
, und eine solche Funktion ist einzigartig. Denn in unserem Beispiel ein catamorphism für reductor
ist sumFold
. Beachten Sie, wie reductor
und sumFold
ähnlich sind: Sie haben fast die gleiche Struktur! In der reductor
Definition entspricht die Verwendung von s
Parametern (deren Typ entspricht T
) der Verwendung des Ergebnisses der Berechnung von sumFold xs
in sumFold
Definition.
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 append
Funktion, 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
appendFold
ist ein catamorphism für appendReductor
welche transformiert IntList
in 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, unfolds
fü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 IntStream
s 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 IntStream
Typs ü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 T
ist ein Typ? Von nun an werden wir definieren
F1 T = Int × T
Nun ist F-Kohlegebra ein Paar (T, g)
, wobei T
es sich um einen Typ und g
eine Funktion des Typs handelt g :: T -> F T
. Zum Beispiel (IntStream, head&tail)
ist eine F1-Kohlegebra. Auch hier, wie in F-Algebren, g
und T
willkü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 IntStream
ist eine terminale F-Kohlegebra. Dies bedeutet , dass für jeden Typ T
und für jede Funktion p :: T -> F1 T
es eine Funktion gibt, genannt Anamorphismus , das umwandelt T
auf 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 nats
und sehen natsBuilder
. Es ist sehr ähnlich zu der Verbindung, die wir zuvor mit Reduktoren und Falten beobachtet haben. nats
ist 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 iterate
ist 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.