Was bedeutet „Kohlegebra“ im Kontext der Programmierung?


339

Ich habe den Begriff "Kohlegebren" in funktionalen Programmier- und PLT-Kreisen mehrmals gehört, insbesondere wenn es um Objekte, Comonaden, Linsen und dergleichen geht. Wenn Sie diesen Begriff googeln, erhalten Sie Seiten mit mathematischen Beschreibungen dieser Strukturen, die für mich so gut wie unverständlich sind. Kann jemand bitte erklären, was Kohlegebren im Kontext der Programmierung bedeuten, welche Bedeutung sie haben und wie sie sich auf Objekte und Komonaden beziehen?


21
Darf ich Jeremy Gibbons 'ausgezeichnetes Buch Patterns in FP empfehlen: patterninfp.wordpress.com und sein durchaus verständliches Papier "Calculating Functional Programs"? Beide behandeln Kohlegebren ziemlich streng (im Vergleich zu z. B. einem Blog-Beitrag), aber sie sind auch für jemanden, der ein bisschen Haskell kennt, ziemlich eigenständig.
Kristopher Micinski

2
@KristopherMicinski, sehr interessant. Vielen Dank!
fehlender Faktor

Antworten:


474

Algebren

Ich denke, der Ausgangspunkt wäre, die Idee einer Algebra zu verstehen . Dies ist nur eine Verallgemeinerung algebraischer Strukturen wie Gruppen, Ringe, Monoide und so weiter. Meistens werden diese Dinge in Form von Sets vorgestellt, aber da wir unter Freunden sind, werde ich stattdessen über Haskell-Typen sprechen. (Ich kann jedoch nicht widerstehen, einige griechische Buchstaben zu verwenden - sie lassen alles cooler aussehen!)

Eine Algebra ist also nur ein Typ τmit einigen Funktionen und Identitäten. Diese Funktionen verwenden unterschiedlich viele Argumente vom Typ τund erzeugen ein τ: ohne Eile sehen sie alle so aus (τ, τ,…, τ) → τ. Sie können auch "Identitäten" haben - Elemente τdavon haben bei einigen Funktionen ein besonderes Verhalten.

Das einfachste Beispiel hierfür ist das Monoid. Ein Monoid ist jeder Typ τmit einer Funktion mappend ∷ (τ, τ) → τund einer Identität mzero ∷ τ. Andere Beispiele sind Dinge wie Gruppen (die wie Monoide sind, außer mit einer zusätzlichen invert ∷ τ → τFunktion), Ringe, Gitter und so weiter.

Alle Funktionen arbeiten τ, können aber unterschiedliche Aritäten haben. Wir können diese als τⁿ → τ, wo τⁿKarten zu einem Tupel von schreiben n τ. Auf diese Weise macht es Sinn , von Identitäten zu denken , wie , τ⁰ → τwo τ⁰nur das leere Tupel ist (). Wir können also die Idee einer Algebra jetzt tatsächlich vereinfachen: Es ist nur ein Typ mit einer Reihe von Funktionen.

Eine Algebra ist nur ein gängiges Muster in der Mathematik, das "herausgerechnet" wurde, genau wie wir es mit Code tun. Die Leute bemerkten, dass eine ganze Reihe interessanter Dinge - die oben genannten Monoide, Gruppen, Gitter usw. - einem ähnlichen Muster folgen, also abstrahierten sie es. Dies hat den gleichen Vorteil wie beim Programmieren: Es erstellt wiederverwendbare Beweise und erleichtert bestimmte Arten des Denkens.

F-Algebren

Mit Factoring sind wir jedoch noch nicht ganz fertig. Bisher haben wir eine Reihe von Funktionen τⁿ → τ. Wir können tatsächlich einen ordentlichen Trick machen, um sie alle in einer Funktion zu kombinieren. Schauen wir uns insbesondere Monoide an: Wir haben mappend ∷ (τ, τ) → τund mempty ∷ () → τ. Wir können diese mit einem Summentyp in eine einzige Funktion umwandeln Either. Es würde so aussehen:

op  Monoid τ  Either (τ, τ) ()  τ
op (Left (a, b)) = mappend (a, b)
op (Right ())    = mempty

Wir können tatsächlich diese Transformation verwenden immer wieder zu vereinen alle die τⁿ → τFunktionen in einer einzigen, für jede Algebra. (In der Tat können wir dies für eine beliebige Anzahl von Funktionen tun a → τ, b → τund so weiter für jede a, b,… .)

Dies lässt uns über Algebren als einen Typ τmit einer einzelnen Funktion sprechen, von einem Durcheinander von Eithers bis zu einer einzelnen τ. Für Monoide ist dieses Durcheinander : Either (τ, τ) (); Für Gruppen (die eine zusätzliche τ → τOperation haben) ist es : Either (Either (τ, τ) τ) (). Es ist ein anderer Typ für jede andere Struktur. Was haben all diese Typen gemeinsam? Das offensichtlichste ist, dass sie alle nur Summen von Produkten sind - algebraische Datentypen. Für Monoide könnten wir beispielsweise einen Monoid-Argumenttyp erstellen, der für jedes Monoid τ funktioniert :

data MonoidArgument τ = Mappend τ τ -- here τ τ is the same as (τ, τ)
                      | Mempty      -- here we can just leave the () out

Wir können dasselbe für Gruppen, Ringe und Gitter und alle anderen möglichen Strukturen tun.

Was ist das Besondere an all diesen Typen? Nun, sie sind alle Functors! Z.B:

instance Functor MonoidArgument where
  fmap f (Mappend τ τ) = Mappend (f τ) (f τ)
  fmap f Mempty        = Mempty

So können wir unsere Vorstellung von einer Algebra noch weiter verallgemeinern. Es ist nur ein Typ τmit einer Funktion f τ → τfür einen Funktor f. Tatsächlich könnten wir dies als Typklasse aufschreiben:

class Functor f  Algebra f τ where
  op  f τ  τ

Dies wird oft als "F-Algebra" bezeichnet, da sie vom Funktor bestimmt wird F. Wenn wir teilweise Typklassen anwenden könnten, könnten wir so etwas definieren class Monoid = Algebra MonoidArgument.

Kohlegebren

Hoffentlich haben Sie jetzt einen guten Überblick darüber, was eine Algebra ist und wie sie nur eine Verallgemeinerung normaler algebraischer Strukturen ist. Was ist eine F-Kohlegebra? Nun, die Co impliziert, dass es das "Dual" einer Algebra ist - das heißt, wir nehmen eine Algebra und drehen einige Pfeile um. Ich sehe nur einen Pfeil in der obigen Definition, also drehe ich das einfach um:

class Functor f  CoAlgebra f τ where
  coop  τ  f τ

Und das ist alles was es ist! Nun mag diese Schlussfolgerung ein wenig flippig erscheinen (heh). Es sagt Ihnen, was eine Kohlegebra ist, gibt aber keinen wirklichen Einblick darüber, wie nützlich es ist oder warum es uns wichtig ist. Ich werde gleich darauf zurückkommen, sobald ich ein oder zwei gute Beispiele gefunden oder gefunden habe: P.

Klassen und Objekte

Nachdem ich ein bisschen herumgelesen habe, denke ich, dass ich eine gute Vorstellung davon habe, wie man Kohlegebren verwendet, um Klassen und Objekte darzustellen. Wir haben einen Typ C, der alle möglichen internen Zustände von Objekten in der Klasse enthält. Die Klasse selbst ist eine Kohlegebra, über Cdie die Methoden und Eigenschaften der Objekte angegeben werden.

Wie im Algebra-Beispiel gezeigt, können wir, wenn wir eine Reihe von Funktionen wie a → τund b → τfür jede haben a, b,…, diese alle unter Verwendung Eithereines Summentyps zu einer einzigen Funktion kombinieren . Der duale „Begriff“ würde eine Reihe von Funktionen des Typs wird kombiniert τ → a, τ → bund so weiter. Wir können dies mit dem Dual eines Summentyps tun - einem Produkttyp. Angesichts der beiden oben genannten Funktionen (aufgerufen fund g) können wir also eine einzige wie folgt erstellen:

both  τ  (a, b)
both x = (f x, g x)

Der Typ (a, a)ist auf einfache Weise ein Funktor, daher passt er sicherlich zu unserer Vorstellung einer F-Kohlegebra. Mit diesem speziellen Trick können wir eine Reihe verschiedener Funktionen - oder für OOP Methoden - in einer einzigen Funktion vom Typ zusammenfassen τ → f τ.

Die Elemente unseres Typs Crepräsentieren den internen Zustand des Objekts. Wenn das Objekt einige lesbare Eigenschaften hat, müssen sie vom Status abhängig sein können. Der naheliegendste Weg, dies zu tun, besteht darin, sie zu einer Funktion von zu machen C. Wenn wir also eine Längeneigenschaft wollen (z. B. object.length), hätten wir eine Funktion C → Int.

Wir wollen Methoden, die ein Argument annehmen und den Status ändern können. Dazu müssen wir alle Argumente aufgreifen und ein neues erstellen C. Stellen wir uns eine setPositionMethode vor, die eine xund eine yKoordinate nimmt : object.setPosition(1, 2). Es würde so aussehen : C → ((Int, Int) → C).

Das wichtige Muster hierbei ist, dass die "Methoden" und "Eigenschaften" des Objekts das Objekt selbst als erstes Argument verwenden. Dies ist genau wie der selfParameter in Python und wie das Implizit thisvieler anderer Sprachen. Eine Kohlegebra kapselt im Wesentlichen nur das Verhalten der Aufnahme eines selfParameters: Das ist der erste Cin C → F C.

Also lasst uns alles zusammenfügen. Stellen wir uns eine Klasse mit einer positionEigenschaft, einer nameEigenschaft und einer setPositionFunktion vor:

class C
  private
    x, y  : Int
    _name : String
  public
    name        : String
    position    : (Int, Int)
    setPosition : (Int, Int)  C

Wir brauchen zwei Teile, um diese Klasse darzustellen. Zunächst müssen wir den internen Zustand des Objekts darstellen. in diesem Fall hält es nur zwei Ints und a String. (Dies ist unser Typ C.) Dann müssen wir uns die Kohlegebra einfallen lassen, die die Klasse darstellt.

data C = Obj { x, y   Int
             , _name  String }

Wir haben zwei Eigenschaften zu schreiben. Sie sind ziemlich trivial:

position  C  (Int, Int)
position self = (x self, y self)

name  C  String
name self = _name self

Jetzt müssen wir nur noch die Position aktualisieren können:

setPosition  C  (Int, Int)  C
setPosition self (newX, newY) = self { x = newX, y = newY }

Dies ist wie eine Python-Klasse mit ihren expliziten selfVariablen. Jetzt, da wir eine Reihe von self →Funktionen haben, müssen wir sie zu einer einzigen Funktion für die Kohlegebra kombinieren. Wir können dies mit einem einfachen Tupel tun:

coop  C  ((Int, Int), String, (Int, Int)  C)
coop self = (position self, name self, setPosition self)

Der Typ ((Int, Int), String, (Int, Int) → c)- für jeden c - ist ein Funktor und coophat daher die gewünschte Form : Functor f ⇒ C → f C.

Vor diesem Hintergrund Czusammen mit coopeiner Kohlegebra, die die Klasse angibt, die ich oben angegeben habe. Sie können sehen, wie wir dieselbe Technik verwenden können, um eine beliebige Anzahl von Methoden und Eigenschaften für unsere Objekte anzugeben.

Auf diese Weise können wir kohlegebraisches Denken verwenden, um mit Klassen umzugehen. Zum Beispiel können wir den Begriff eines "F-Kohlegebra-Homomorphismus" einbringen, um Transformationen zwischen Klassen darzustellen. Dies ist ein beängstigend klingender Begriff, der nur eine Transformation zwischen Kohlegebren bedeutet, die die Struktur bewahrt. Dies erleichtert das Zuordnen von Klassen zu anderen Klassen erheblich.

Kurz gesagt, eine F-Kohlegebra stellt eine Klasse dar, indem sie eine Reihe von Eigenschaften und Methoden aufweist, die alle von einem selfParameter abhängen , der den internen Zustand jedes Objekts enthält.

Andere Kategorien

Bisher haben wir über Algebren und Kohlegebren als Haskell-Typen gesprochen. Eine Algebra ist nur ein Typ τmit einer Funktion f τ → τund eine Kohlegebra ist nur ein Typ τmit einer Funktion τ → f τ.

Nichts verbindet diese Ideen jedoch wirklich mit Haskell an sich . Tatsächlich werden sie normalerweise eher in Form von Mengen und mathematischen Funktionen als in Form von Typen und Haskell-Funktionen eingeführt. In der Tat können wir diese Konzepte auf beliebige Kategorien verallgemeinern !

Wir können eine F-Algebra für eine Kategorie definieren C. Erstens brauchen wir einen Funktor F : C → C- das heißt einen Endofunktor . (Alle Haskell Functors sind eigentlich endofunctors aus Hask → Hask.) Dann wird eine Algebra ist nur ein Objekt Aaus Cmit einem Morphismus F A → A. Eine Kohlegebra ist die gleiche außer mit A → F A.

Was gewinnen wir, wenn wir andere Kategorien berücksichtigen? Nun, wir können dieselben Ideen in verschiedenen Kontexten verwenden. Wie Monaden. In Haskell ist eine Monade ein Typ M ∷ ★ → ★mit drei Operationen:

map         β)  (M α  M β)
return    α  M α
join      M (M α)  M α

Die mapFunktion ist nur ein Beweis dafür, dass Ma Functor. Wir können also sagen, dass eine Monade nur ein Funktor mit zwei Operationen ist: returnund join.

Funktoren bilden selbst eine Kategorie, wobei Morphismen zwischen ihnen sogenannte "natürliche Transformationen" sind. Eine natürliche Transformation ist nur eine Möglichkeit, einen Funktor in einen anderen zu verwandeln und dabei seine Struktur zu erhalten. Hier ist ein schöner Artikel, der die Idee erklärt. Es spricht darüber concat, was nur joinfür Listen ist.

Bei Haskell-Funktoren ist die Zusammensetzung von zwei Funktoren selbst ein Funktor. Im Pseudocode könnten wir Folgendes schreiben:

instance (Functor f, Functor g)  Functor (f  g) where
  fmap fun x = fmap (fmap fun) x

Dies hilft uns, joinals Mapping von zu denken f ∘ f → f. Die Art von joinist ∀α. f (f α) → f α. Intuitiv können wir sehen, wie eine für alle Typen gültige Funktion αals Transformation von betrachtet werden kann f.

returnist eine ähnliche Transformation. Sein Typ ist ∀α. α → f α. Das sieht anders aus - der erste αist nicht "in" einem Funktor! Glücklicherweise können wir dies beheben, indem wir dort einen Identitätsfunktor hinzufügen : ∀α. Identity α → f α. So returnist eine Transformation Identity → f.

Jetzt können wir uns eine Monade als eine Algebra vorstellen, die auf einem Funktor fmit Operationen f ∘ f → fund basiert Identity → f. Kommt Ihnen das nicht bekannt vor? Es ist sehr ähnlich zu einem Monoid, das nur eine Art τmit Operationen τ × τ → τund war () → τ.

Eine Monade ist also wie ein Monoid, nur dass wir anstelle eines Typs einen Funktor haben. Es ist die gleiche Art von Algebra, nur in einer anderen Kategorie. (Hier kommt, soweit ich weiß, der Satz "Eine Monade ist nur ein Monoid in der Kategorie der Endofunktoren" her.)

Jetzt haben wir diese beiden Operationen: f ∘ f → fund Identity → f. Um die entsprechende Kohlegebra zu erhalten, drehen wir einfach die Pfeile. Dies gibt uns zwei neue Operationen: f → f ∘ fund f → Identity. Wir können sie in Haskell-Typen umwandeln, indem wir wie oben Typvariablen hinzufügen und uns ∀α. f α → f (f α)und geben ∀α. f α → α. Dies sieht genauso aus wie die Definition einer Comonade:

class Functor f  Comonad f where
  coreturn  f α  α
  cojoin    f α  f (f α)

Eine Comonade ist also eine Kohlegebra in einer Kategorie von Endofunktoren.


45
Das ist unglaublich wertvoll. Ich hatte es geschafft, einige Intuitionen über dieses ganze F-Algebra-Geschäft durch Lesen und Beispiele (z. B. durch ihre Verwendung bei Katamoprhismen) zu vagen, aber das ist selbst für mich kristallklar. Vielen Dank!
Luis Casillas

28
Dies ist eine großartige Erklärung.
Edward KMETT

5
@ EdwardKmett: Danke. Ist das, was ich über Klassen und Objekte hinzugefügt habe, in Ordnung? Ich habe heute nur darüber gelesen, aber es scheint sinnvoll zu sein.
Tikhon Jelvis

7
Für das, was es wert ist: Die "Kategorie der Endofunktoren" ist hier genauer eine Kategorie, deren Objekte Endofunktoren in einer bestimmten Kategorie sind und deren Pfeile natürliche Transformationen sind. Dies ist eine monoidale Kategorie, deren Funktorkomposition (,)dem Identitätsfunktor entspricht (). Ein Monoidobjekt innerhalb einer Monoidkategorie ist ein Objekt mit Pfeilen, die Ihrer Monoidalgebra entsprechen und ein Monoidobjekt in Hask mit Produkttypen als Monoidstruktur beschreiben. Ein monoides Objekt in der Kategorie der Endofunktoren auf C ist eine Monade auf C, also ja, Ihr Verständnis ist korrekt. :]
CA McCann

8
Das war ein episches Ende!
jdinunzio

85

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.


Die Art und Definition von appendReductorsieht etwas seltsam aus und hat mir nicht wirklich geholfen, das Muster dort zu erkennen ... :) Können Sie noch einmal überprüfen, ob es korrekt ist? .. Wie sollten Reduktortypen im Allgemeinen aussehen? Wird in der Definition von rdort F1durch die IntList bestimmt, oder ist es ein beliebiges F?
Max Galkin

37

Das Tutorial durcharbeiten Ein Tutorial zu (Co) Algebren und (Co) Induktion soll Ihnen einen Einblick in die Co-Algebra in der Informatik geben.

Unten finden Sie ein Zitat davon, um Sie zu überzeugen,

Im Allgemeinen manipuliert ein Programm in einer Programmiersprache Daten. Während der Entwicklung der Informatik in den letzten Jahrzehnten wurde klar, dass eine abstrakte Beschreibung dieser Daten wünschenswert ist, um beispielsweise sicherzustellen, dass das eigene Programm nicht von der besonderen Darstellung der Daten abhängt, mit denen es arbeitet. Eine solche Abstraktheit erleichtert auch Korrektheitsnachweise.
Dieser Wunsch führte zur Verwendung algebraischer Methoden in der Informatik in einem Zweig, der als algebraische Spezifikation oder abstrakte Datentypentheorie bezeichnet wird. Gegenstand der Studie sind Datentypen an sich, die Begriffe von Techniken verwenden, die aus der Algebra bekannt sind. Die von Informatikern verwendeten Datentypen werden häufig aus einer bestimmten Sammlung von (Konstruktor-) Operationen generiert, und aus diesem Grund spielt die "Initialität" von Algebren eine so wichtige Rolle.
Standardalgebraische Techniken haben sich als nützlich erwiesen, um verschiedene wesentliche Aspekte der in der Informatik verwendeten Datenstrukturen zu erfassen. Es stellte sich jedoch als schwierig heraus, einige der inhärent dynamischen Strukturen, die beim Rechnen auftreten, algebraisch zu beschreiben. Solche Strukturen beinhalten normalerweise einen Zustandsbegriff, der auf verschiedene Arten transformiert werden kann. Formale Ansätze für solche zustandsbasierten dynamischen Systeme verwenden im Allgemeinen Automaten oder Übergangssysteme als klassische frühe Referenzen.
Während des letzten Jahrzehnts wuchs allmählich die Erkenntnis, dass solche zustandsbasierten Systeme nicht als Algebren, sondern als sogenannte Co-Algebren bezeichnet werden sollten. Dies ist das formale Dual von Algebren, auf eine Weise, die in diesem Tutorial präzisiert wird. Die doppelte Eigenschaft der "Initialität" für Algebren, nämlich die Finalität, erwies sich für solche Co-Algebren als entscheidend. Und das logische Argumentationsprinzip, das für solche endgültigen Co-Algebren benötigt wird, ist nicht Induktion, sondern Co-Induktion.


Vorspiel über Kategorietheorie. Die Kategorietheorie sollte in Theorie der Funktoren umbenannt werden. Als Kategorien muss man definieren, um Funktoren zu definieren. (Darüber hinaus sind Funktoren das, was man definieren muss, um natürliche Transformationen zu definieren.)

Was ist ein Funktor? Es ist eine Transformation von einem Satz zum anderen, die ihre Struktur bewahrt. (Für mehr Details gibt es viele gute Beschreibungen im Netz).

Was ist eine F-Algebra? Es ist die Algebra des Funktors. Es ist nur das Studium der universellen Angemessenheit des Funktors.

Wie kann es eine Verbindung zur Informatik sein? Das Programm kann als strukturierter Informationssatz angezeigt werden. Die Programmausführung entspricht der Änderung dieses strukturierten Informationssatzes. Es klingt gut, dass die Ausführung die Programmstruktur beibehalten sollte. Die Ausführung kann dann als Anwendung eines Funktors über diesen Informationssatz angesehen werden. (Derjenige, der das Programm definiert).

Warum F-Co-Algebra? Programme sind im Wesentlichen dual, da sie durch Informationen beschrieben werden und darauf reagieren. Dann können hauptsächlich die Informationen, aus denen das Programm besteht und die geändert werden, auf zwei Arten angezeigt werden.

  • Daten, die als die vom Programm verarbeiteten Informationen definiert werden können.
  • Status, der als die Informationen definiert werden kann, die vom Programm gemeinsam genutzt werden.

Dann möchte ich an dieser Stelle sagen:

  • F-Algebra ist das Studium der funktionellen Transformation, die über das Datenuniversum wirkt (wie hier definiert).
  • F-Co-Algebren sind die Untersuchung der funktionellen Transformation, die auf das Universum des Staates einwirkt (wie hier definiert).

Während der Laufzeit eines Programms existieren Daten und Status nebeneinander und ergänzen sich gegenseitig. Sie sind dual.


5

Ich werde mit Dingen beginnen, die offensichtlich mit der Programmierung zu tun haben, und dann einige mathematische Dinge hinzufügen, um sie so konkret und bodenständig wie möglich zu halten.


Lassen Sie uns einige Informatiker zur Koinduktion zitieren…

http://www.cs.umd.edu/~micinski/posts/2012-09-04-on-understanding-coinduction.html

Bei der Induktion geht es um endliche Daten, bei der Co-Induktion um unendliche Daten.

Das typische Beispiel für unendliche Daten ist der Typ einer Lazy List (eines Streams). Nehmen wir zum Beispiel an, wir haben das folgende Objekt im Speicher:

 let (pi : int list) = (* some function which computes the digits of
 π. *)

Der Computer kann nicht alle π aufnehmen, da er nur eine begrenzte Menge an Speicher hat! Aber was es tun kann, ist ein endliches Programm zu halten, das jede beliebig lange Erweiterung von π erzeugt, die Sie wünschen. Solange Sie nur endliche Teile der Liste verwenden, können Sie mit dieser unendlichen Liste so viel berechnen, wie Sie benötigen.

Beachten Sie jedoch das folgende Programm:

let print_third_element (k : int list) =   match k with
     | _ :: _ :: thd :: tl -> print thd


 print_third_element pi

Dieses Programm sollte die dritte Ziffer von pi drucken. In einigen Sprachen wird jedoch jedes Argument für eine Funktion ausgewertet, bevor es an eine Funktion übergeben wird (strikte, nicht faule Auswertung). Wenn wir diese Reduzierungsreihenfolge verwenden, wird unser oben beschriebenes Programm für immer ausgeführt und berechnet die Ziffern von pi, bevor es an unsere Druckerfunktion übergeben werden kann (was niemals geschieht). Da der Computer nicht über unendlich viel Speicher verfügt, wird dem Programm möglicherweise der Speicher ausgehen und es stürzt ab. Dies ist möglicherweise nicht die beste Bewertungsreihenfolge.

http://adam.chlipala.net/cpdt/html/Coinductive.html

In faulen funktionalen Programmiersprachen wie Haskell gibt es überall unendliche Datenstrukturen. Unendliche Listen und exotischere Datentypen bieten praktische Abstraktionen für die Kommunikation zwischen Teilen eines Programms. Das Erreichen einer ähnlichen Bequemlichkeit ohne unendliche träge Strukturen würde in vielen Fällen akrobatische Umkehrungen des Kontrollflusses erfordern.

http://www.alexandrasilva.org/#/talks.html Beispiele für Kohlegebren von Alexandra Silva


Beziehen des mathematischen Umgebungskontexts auf übliche Programmieraufgaben

Was ist "eine Algebra"?

Algebraische Strukturen sehen im Allgemeinen so aus:

  1. Zeug
  2. Was das Zeug kann

Dies sollte wie Objekte mit 1. Eigenschaften und 2. Methoden klingen. Oder noch besser, es sollte wie Typensignaturen klingen.

Zu den mathematischen Standardbeispielen gehören Monoid ⊃ Gruppe ⊃ Vektorraum ⊃ "eine Algebra". Monoide sind wie Automaten: Folgen von Verben (z f.g.h.h.nothing.f.g.f. B. ). Ein gitProtokoll, das immer den Verlauf hinzufügt und ihn niemals löscht, ist ein Monoid, aber keine Gruppe. Wenn Sie Inverse hinzufügen (z. B. negative Zahlen, Brüche, Wurzeln, Löschen des akkumulierten Verlaufs, Aufbrechen eines zerbrochenen Spiegels), erhalten Sie eine Gruppe.

Gruppen enthalten Dinge, die addiert oder subtrahiert werden können. Zum Beispiel können Durations addiert werden. (Aber Dates kann nicht.) Die Dauer lebt in einem Vektorraum (nicht nur in einer Gruppe), da sie auch durch externe Zahlen skaliert werden kann. (Eine Typensignatur von scaling :: (Number,Duration) → Duration.)

Algebren ⊂ Vektorräume können noch etwas anderes: Es gibt einige m :: (T,T) → T. Nennen Sie dies "Multiplikation" oder nicht, denn sobald Sie gehen, ist Integerses weniger offensichtlich, was "Multiplikation" (oder "Exponentiation" ) sein sollte.

(Aus diesem Grund achten die Menschen auf (kategorietheoretische) universelle Eigenschaften: um ihnen zu sagen, wie Multiplikation funktionieren soll oder sein soll :

universelle Eigenschaft des Produkts )


Algebren → Coalgebren

Comultiplication ist einfacher auf eine Weise zu definieren, die sich nicht willkürlich anfühlt, als Multiplikation, da T → (T,T)Sie einfach dasselbe Element wiederholen können. ("diagonale Karte" - wie diagonale Matrizen / Operatoren in der Spektraltheorie)

Counit ist normalerweise die Spur (Summe der diagonalen Einträge), obwohl wiederum wichtig ist, was Ihr Counit tut ; traceist nur eine gute Antwort für Matrizen.

Der Grund, einen dualen Raum im Allgemeinen zu betrachten, liegt darin, dass es einfacher ist, in diesem Raum zu denken. Zum Beispiel ist es manchmal einfacher, an einen normalen Vektor zu denken als an die Ebene, für die er normal ist, aber Sie können Ebenen (einschließlich Hyperebenen) mit Vektoren steuern (und jetzt spreche ich von dem bekannten geometrischen Vektor, wie in einem Raytracer). .


(Un) strukturierte Daten zähmen

Mathematiker modellieren möglicherweise etwas, das Spaß macht, wie TQFTs , während Programmierer damit zu kämpfen haben

  • Datum / Uhrzeit ( + :: (Date,Duration) → Date),
  • Orte ( Paris(+48.8567,+2.3508)! Es ist eine Form, kein Punkt.),
  • unstrukturierter JSON, der in gewissem Sinne konsistent sein soll,
  • falsches, aber nahes XML,
  • unglaublich komplexe GIS-Daten, die viele sinnvolle Beziehungen erfüllen sollten,
  • reguläre Ausdrücke, die Ihnen etwas bedeuteten , Perl aber wesentlich weniger bedeuteten.
  • CRM, das alle Telefonnummern und Villenstandorte der Führungskraft, die Namen seiner (jetzt Ex-) Frau und Kinder, den Geburtstag und alle vorherigen Geschenke enthalten sollte, von denen jedes "offensichtliche" Beziehungen (für den Kunden offensichtlich) erfüllen sollte, die unglaublich sind schwer zu codieren,
  • ..... .....

Wenn Informatiker über Kohlegebren sprechen, denken sie normalerweise an festgelegte Operationen wie das kartesische Produkt. Ich glaube, das meinen die Leute, wenn sie sagen: "Algebren sind Kohlegebren in Haskell". Aber in dem Maße , dass Programmierer wie komplexe Datentypen zu modellieren haben Place, Date/Timeund Customer-und diese Modelle wie die reale Welt aussehen so viel machen (oder zumindest die Endbenutzers Sicht der realen Welt) wie möglich, ich glaube duals, könnte über die Set-Welt hinaus nützlich sein.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.