it-swarm.com.de

Was bedeutet "Kohlegebra" im Kontext der Programmierung?

Ich habe den Begriff "Kohlegebren" mehrmals in funktionalen Programmier- und PLT-Kreisen gehört, insbesondere wenn es um Objekte, Komonaden, 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 mir jemand erklären, was Kohlegebren im Kontext der Programmierung bedeuten, welche Bedeutung sie haben und wie sie sich auf Objekte und Comonaden beziehen?

330
missingfaktor

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 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 nehmen unterschiedlich viele Argumente vom Typ τ Und erzeugen einen τ: Uncurried sehen sie alle aus wie (τ, τ,…, τ) → τ. Sie können auch "Identitäten" haben - Elemente von τ, Die sich bei einigen Funktionen besonders verhalten.

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

Alle Funktionen arbeiten mit τ, Können jedoch unterschiedliche Aritäten haben. Wir können diese als τⁿ → τ Ausgeben, wobei τⁿ Einem Tupel von nτ Zugeordnet ist. Auf diese Weise ist es sinnvoll, sich Identitäten als τ⁰ → τ Vorzustellen, wobei τ⁰ Nur das leere Tupel () Ist. Daher können wir die Idee einer Algebra jetzt tatsächlich vereinfachen: Es handelt sich nur um einen Typ mit einer Reihe von Funktionen.

Eine Algebra ist in der Mathematik nur ein verbreitetes Muster, das genau wie beim Code "ausgeklammert" wurde. Die Leute bemerkten, dass eine ganze Reihe von interessanten Dingen - die oben genannten Monoide, Gruppen, Gitter usw. - alle einem ähnlichen Muster folgen, so dass sie es abstrahierten. Dies hat den gleichen Vorteil wie das Programmieren: Es erstellt wiederverwendbare Beweise und erleichtert bestimmte Argumentationsarten.

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. Betrachten wir insbesondere Monoide: Wir haben mappend ∷ (τ, τ) → τ und mempty ∷ () → τ. Wir können diese mit einem Summentyp —Either in eine einzige Funktion umwandeln. Es würde so aussehen:

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

Wir können diese Transformation tatsächlich wiederholt verwenden, um all die τⁿ → τ - Funktionen in einer einzigen zu kombinieren, für any Algebra. (Tatsächlich können wir dies für eine beliebige Anzahl von Funktionen a → τ, b → τ Usw. für anya, b,… Tun.)

Auf diese Weise können wir über Algebren als Typ τ Mit einer single - Funktion von einem Durcheinander von Eithers zu einem einzelnen τ Sprechen. Für Monoide ist dieses Durcheinander: Either (τ, τ) (); Für Gruppen (die eine zusätzliche τ → τ - Operation haben) ist dies: Either (Either (τ, τ) τ) (). Es ist ein anderer Typ für jede andere Struktur. Was haben all diese Typen gemeinsam? Das offensichtlichste ist, dass es sich bei allen nur um Summen von Produkten handelt - algebraische Datentypen. Zum Beispiel könnten wir für Monoide einen monoiden Argumenttyp erstellen, der für any 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 und 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. In der Tat könnten wir dies als eine Typenklasse ausschreiben:

class Functor f ⇒ Algebra f τ where
  op ∷ f τ → τ

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

Kohlengebren

Nun haben Sie hoffentlich einen guten Überblick darüber, was eine Algebra ist und wie sie nur eine Verallgemeinerung normaler algebraischer Strukturen darstellt. Was ist eine F-Kohlegebra? Nun, die Co impliziert, dass es das "Doppel" einer Algebra ist - das heißt, wir nehmen eine Algebra und drehen ein paar Pfeile um. Ich sehe in der obigen Definition nur einen Pfeil, 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, wie nützlich es ist oder warum es uns interessiert. Ich werde gleich darauf zurückkommen, wenn ich ein oder zwei gute Beispiele gefunden oder gefunden habe: P.

Klassen und Objekte

Nachdem ich ein wenig gelesen habe, denke ich, dass ich eine gute Idee 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 Coalgebra über C, die die Methoden und Eigenschaften der Objekte angibt.

Wie im Algebra-Beispiel gezeigt, können wir, wenn wir eine Reihe von Funktionen wie a → τ Und b → τ Für eine beliebige a, b,… Haben, diese mit Either, ein Summentyp. Der doppelte "Begriff" würde eine Reihe von Funktionen vom Typ τ → a, τ → b Und so weiter kombinieren. Wir können dies mit dem Dual eines Summentyps - eines Produkttyps - tun. Mit den beiden oben genannten Funktionen (f und g) können wir also eine einzige 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 von einer F-Kohlegebra. Mit diesem speziellen Trick können wir eine Reihe verschiedener Funktionen - oder für OOP-Methoden - in eine einzige Funktion vom Typ τ → f τ Packen.

Die Elemente unseres Typs C repräsentieren den internal Status des Objekts. Wenn das Objekt über lesbare Eigenschaften verfügt, müssen diese vom Status abhängen können. Der naheliegendste Weg, dies zu tun, besteht darin, sie zu einer Funktion von C zu machen. 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 Zustand ändern können. Dazu müssen wir alle Argumente nehmen und ein neues C erzeugen. Stellen wir uns eine setPosition Methode vor, die eine x und eine y Koordinate annimmt: 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 entspricht dem self -Parameter in Python und dem impliziten this vieler anderer Sprachen. Eine Kohlegebra kapselt im Wesentlichen nur das Verhalten der Aufnahme eines self Parameter: Das ist der erste C in C → F C.

Also lassen Sie uns alles zusammenfassen. Stellen wir uns eine Klasse mit einer Eigenschaft position, einer Eigenschaft name und einer Funktion setPosition 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 enthält es nur zwei Ints und ein String. (Dies ist unser Typ C.) Dann müssen wir uns die Kohlegebra ausdenken, die die Klasse repräsentiert.

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 self Variablen. Da wir nun eine Reihe von self → Funktionen haben, müssen wir sie zu einer einzigen kombinieren Funktion für die Kohlegebra. 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 anyc - ist ein Funktor, also hat coop die gewünschte Form: Functor f ⇒ C → f C.

Vor diesem Hintergrund bilden C zusammen mit coop eine Kohlebra, 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 kohlebraische Argumente 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 enthält, die alle von einem self -Parameter 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 τ.

Jedoch verbindet nichts wirklich diese Ideen mit Haskell per se. Tatsächlich werden sie normalerweise in Form von Mengen und mathematischen Funktionen anstelle von Typen und Haskell-Funktionen eingeführt. In der Tat können wir diese Konzepte auf any Kategorien verallgemeinern!

Wir können eine F-Algebra für eine Kategorie C definieren. Zuerst brauchen wir einen Funktor F : C → C - also einen Endofunktor. (Alle Haskell Functors sind tatsächlich Endofunktoren von Hask → Hask.) Dann ist eine Algebra nur ein Objekt A von C mit einem Morphismus F A → A. Eine Kohlegebra ist dasselbe mit Ausnahme von A → F A.

Was gewinnen wir durch die Berücksichtigung anderer Kategorien? Nun, wir können die gleichen Ideen in verschiedenen Kontexten verwenden. Wie Monaden. In Haskell ist eine Monade eine Art M ∷ ★ → ★ Mit drei Operationen:

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

Die Funktion map ist nur ein Beweis dafür, dass M ein Functor ist. Wir können also sagen, dass eine Monade nur ein Funktor mit zwei Operationen ist: return und join.

Functors 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 netter Artikel, der die Idee erklärt. Es geht um concat, was für Listen nur join 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, join als Zuordnung von f ∘ f → f Zu betrachten. Der Typ von join ist ∀α. f (f α) → f α. Intuitiv können wir sehen, wie eine für all Typen α Gültige Funktion als Transformation von f aufgefasst werden kann.

return ist eine ähnliche Transformation. Sein Typ ist ∀α. α → f α. Dies sieht anders aus - das erste α Ist kein "in" Funktor! Glücklicherweise können wir dies beheben, indem wir dort einen Identitäts-Funktor hinzufügen: ∀α. Identity α → f α. Also ist return eine Transformation Identity → f.

Jetzt können wir uns eine Monade als eine Algebra vorstellen, die auf einem Funktor f mit den Operationen f ∘ f → f Und Identity → f Basiert. Kommt Ihnen das nicht bekannt vor? Es ist einem Monoid sehr ähnlich, das nur eine Art τ Mit Operationen τ × τ → τ Und () → τ War.

Eine Monade ist also genau wie eine Monoide, außer dass wir anstelle eines Typs einen Funktor haben. Es ist die gleiche Art von Algebra, nur in einer anderen Kategorie. (Hier kommt meines Wissens der Satz "Eine Monade ist nur ein Monoid in der Kategorie der Endofunktoren" her.)

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

class Functor f ⇒ Comonad f where
  coreturn ∷ f α → α
  cojoin   ∷ f α → f (f α)

Ein Comonad ist also eine coalgebra in einer Kategorie von Endofunktoren.

465
Tikhon Jelvis

F-Algebren und F-Kohlegebren sind mathematische Strukturen, die für das Denken über induktive Typen (oder rekursive Typen) von Bedeutung sind.

F-Algebren

Wir werden zuerst mit F-Algebren beginnen. 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 ganzen Zahlen:

data IntList = Nil | Cons (Int, IntList)

Es ist offensichtlich, dass es rekursiv ist - in der Tat bezieht sich seine Definition auf sich selbst. Seine Definition besteht aus zwei Datenkonstruktoren, die folgende Typen haben:

Nil  :: () -> IntList
Cons :: (Int, IntList) -> IntList

Beachten Sie, dass ich den Typ Nil als () -> IntList Geschrieben habe, nicht einfach IntList. Dies sind theoretisch ä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 ist 1 eine Einheitensammlung (gesetzt mit einem Element) und die Operation A × B ein Kreuzprodukt aus zwei Mengen A und B (dh Menge) Paar (a, b) wobei a alle Elemente von A und b alle Elemente von B durchläuft).

Disjunkte Vereinigung zweier Mengen A und B ist eine Menge A | B, Die eine Vereinigung von Mengen {(a, 1) : a in A} und {(b, 2) : b in B} ist. Im Wesentlichen ist es eine Menge aller Elemente aus A und B, wobei jedoch jedes dieser Elemente als zu A oder B gehörend 'markiert' ist. Wenn wir also ein Element aus A | B auswählen, wissen wir sofort, ob dieses Element aus A oder aus B stammt.

Wir können die Funktionen Nil und Cons 'verbinden', so dass sie eine einzige Funktion bilden, die an einer Menge 1 | (Int × IntList) arbeitet:

Nil|Cons :: 1 | (Int × IntList) -> IntList

Wenn die Funktion Nil|Cons Auf den Wert () Angewendet wird (der offensichtlich zu 1 | (Int × IntList) set gehört), verhält es sich so, als wäre es Nil. ; Wenn Nil|Cons auf einen Wert vom Typ (Int, IntList) angewendet wird (diese Werte befinden sich auch in der Menge 1 | (Int × IntList)), verhält es sich wie 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 zusammengefasst werden können:

Leaf|Branch :: Int | (IntTree × IntTree) -> IntTree

Es ist ersichtlich, dass beide Funktionen joined einen ähnlichen Typ haben: Sie sehen beide so aus

f :: F T -> T

dabei ist F eine Art Transformation, die unseren Typ übernimmt und komplexeren Typ ergibt, der aus x - und | - Operationen, Verwendungen von T und möglicherweise besteht andere Arten. Beispielsweise sieht für IntList und IntTreeF folgendermaßen 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 "algebraisch" genannt: Sie bestehen aus einer Reihe von "Summen" (Gewerkschaften) und "Produkten" (Kreuzprodukten) anderer Typen.

Nun können wir die F-Algebra definieren. F-Algebra ist nur ein Paar (T, f), Wobei T ein Typ ist und f eine Funktion des Typs f :: F T -> T Ist. 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 T und f selbst beliebig sein können. Zum Beispiel sind (String, g :: 1 | (Int x String) -> String) Oder (Double, h :: Int | (Double, Double) -> Double) Für einige g und h auch F-Algebren für entsprechende F.

Danach können wir F-Algebra Homomorphismen und dann anfängliche F-Algebren einführen, die sehr nützliche Eigenschaften haben. Tatsächlich ist (IntList, Nil|Cons) 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 sind als nötig.

Die Tatsache, dass beispielsweise (IntList, Nil|Cons) Eine F-Algebra ist, ermöglicht es uns jedoch, fold - ähnliche Funktionen für diesen Typ zu definieren. Wie Sie wissen, ist fold eine Art von Operation, die einen rekursiven Datentyp in einen endlichen Wert umwandelt. Zum Beispiel können wir eine Liste mit ganzen Zahlen zu einem einzigen Wert falten, der eine Summe aller Elemente in der Liste darstellt:

foldr (+) 0 [1, 2, 3, 4] -> 1 + 2 + 3 + 4 = 10

Es ist möglich, eine solche Operation auf jeden rekursiven Datentyp zu verallgemeinern.

Das Folgende ist eine Signatur der Funktion foldr:

foldr :: ((a -> b -> b), b) -> [a] -> b

Beachten Sie, dass ich die ersten beiden Argumente mit geschweiften Klammern vom letzten getrennt habe. Dies ist keine echte foldr - Funktion, sondern eine isomorphe Funktion (dh Sie können leicht eine von der anderen abrufen und umgekehrt). Teilweise angewendet foldr hat die folgende Signatur:

foldr ((+), 0) :: [Int] -> Int

Wir können sehen, dass dies eine Funktion ist, die eine Liste von ganzen Zahlen nimmt und eine einzelne ganze Zahl zurückgibt. Definieren wir eine solche Funktion anhand unseres Typs IntList.

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 im Nil - Teil von IntList und der zweite Teil definiert das Verhalten der Funktion im Cons - Teil.

Nehmen wir nun an, wir programmieren nicht in Haskell, sondern in einer Sprache, die die Verwendung algebraischer Typen direkt in Typensignaturen erlaubt (technisch gesehen erlaubt Haskell die Verwendung algebraischer Typen über Tupel und Either a b - Datentyp, was jedoch zu unnötigen Ergebnissen führt Ausführlichkeit). Betrachten Sie eine Funktion:

reductor :: () | (Int × Int) -> Int
reductor ()     = 0
reductor (x, s) = x + s

Es ist zu sehen, dass reductor eine Funktion vom Typ F1 Int -> Int Ist, genau wie in der Definition der F-Algebra! In der Tat ist das Paar (Int, reductor) Eine F1-Algebra.

Da IntList eine anfängliche F1-Algebra ist, gibt es für jeden Typ T und für jede Funktion r :: F1 T -> T Eine Funktion mit der Bezeichnung Katamorphismus für r, das IntList in T konvertiert, und diese Funktion ist eindeutig. Tatsächlich ist in unserem Beispiel ein Katamorphismus für reductorsumFold. Beachten Sie, dass reductor und sumFold ähnlich sind: Sie haben fast die gleiche Struktur! In der Definition reductor entspricht die Verwendung des Parameters s (dessen Typ T entspricht) der Verwendung des Ergebnisses der Berechnung von sumFold xs In sumFold Definition.

Nur um es klarer zu machen und Ihnen zu helfen, das Muster zu erkennen, ist hier ein weiteres Beispiel, und wir beginnen wieder mit der resultierenden Faltfunktion. Betrachten Sie die Funktion append, die das erste 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 aus:

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 Katamorphismus für appendReductor, der IntList in IntList umwandelt.

Im Grunde genommen ermöglichen uns F-Algebren, 'Falten' auf rekursiven Datenstrukturen zu definieren, d. H. Operationen, die unsere Strukturen auf einen gewissen Wert reduzieren.

F-Kohlegebren

F-Kohlegebren sind sogenannte "Doppelter" für F-Algebren. Sie ermöglichen es uns, unfolds für rekursive Datentypen zu definieren, dh eine Möglichkeit, 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 Bezug auf Mengen

Cons :: Int × IntStream -> IntStream

Mit Haskell können Sie Mustervergleiche für Datenkonstruktoren durchführen, sodass Sie die folgenden Funktionen definieren können, die für IntStream s gelten:

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 IntStream -> Int × IntStream "Verbinden":

head&tail :: IntStream -> Int × IntStream
head&tail (Cons (x, xs)) = (x, xs)

Beachten Sie, dass das Ergebnis der Funktion mit der algebraischen Darstellung unseres Typs IntStream übereinstimmt. Ähnliches kann auch für andere rekursive Datentypen durchgeführt werden. Vielleicht haben Sie das Muster schon bemerkt. Ich beziehe mich auf eine Familie von Funktionen des Typs

g :: T -> F T

dabei ist T ein Typ. Von nun an werden wir definieren

F1 T = Int × T

Nun ist F-Kohlegebra ein Paar (T, g), Wobei T ein Typ und g eine Funktion des Typs g :: T -> F T Ist. . Zum Beispiel ist (IntStream, head&tail) Eine F1-Kohlegebra. Ebenso wie in F-Algebren können g und T beliebig sein, z. B. ist (String, h :: String -> Int x String) Für einige h auch eine F1-Kohlegebra.

Unter allen F-Kohlegebren gibt es sogenannte terminale F-Kohlegebren, die zu anfänglichen F-Algebren dual sind. Beispielsweise ist IntStream eine terminale F-Kohlegebra. Dies bedeutet, dass für jeden Typ T und für jede Funktion p :: T -> F1 T Eine Funktion namens Anamorphismus existiert, die T in IntStream, und diese Funktion ist einzigartig.

Betrachten Sie die folgende Funktion, die ausgehend von der angegebenen einen Datenstrom aufeinanderfolgender Ganzzahlen generiert:

nats :: Int -> IntStream
nats n = Cons (n, nats (n+1))

Schauen wir uns nun eine Funktion natsBuilder :: Int -> F1 Int An, nämlich natsBuilder :: Int -> Int × Int:

natsBuilder :: Int -> Int × Int
natsBuilder n = (n, n+1)

Auch hier können wir Ähnlichkeiten zwischen nats und natsBuilder feststellen. Es ist sehr ähnlich zu der Verbindung, die wir zuvor mit Reduktoren und Falten beobachtet haben. nats ist ein Anamorphismus für natsBuilder.

Ein anderes Beispiel, eine Funktion, die einen Wert und eine Funktion annimmt und einen Strom aufeinanderfolgender Anwendungen der Funktion an den Wert zurückgibt:

iterate :: (Int -> Int) -> Int -> IntStream
iterate f n = Cons (n, iterate f (f n))

Seine Builder-Funktion ist die folgende:

iterateBuilder :: (Int -> Int) -> Int -> Int × Int
iterateBuilder f n = (n, f n)

Dann ist iterate ein Anamorphismus für iterateBuilder.

Fazit

Kurz gesagt, F-Algebren erlauben es, Falten zu definieren, dh Operationen, die die rekursive Struktur auf einen einzigen Wert reduzieren, und F-Kohlegebren erlauben das Gegenteil: Konstruieren einer [potentiell] unendlichen 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" Wertes in jedem Typ ist. In Haskell können also sowohl Faltungen als auch Entfaltungen für jeden rekursiven Typ erstellt werden. Das theoretische Modell dahinter ist jedoch komplexer als das, das ich oben vorgestellt habe, und deshalb habe ich es absichtlich vermieden.

Hoffe das hilft.

82

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

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

Im Allgemeinen manipuliert ein Programm in einigen Programmiersprachen Daten. Während der Entwicklung der Informatik in den letzten Jahrzehnten wurde deutlich, dass eine abstrakte Beschreibung dieser Daten wünschenswert ist, um beispielsweise sicherzustellen, dass das eigene Programm nicht von der jeweiligen Darstellung der Daten abhängt, mit denen es arbeitet. Eine solche Abstraktheit erleichtert auch Korrektheitsnachweise.
Dieser Wunsch führte zur Anwendung algebraischer Methoden in der Informatik in einem Zweig, der als algebraische Spezifikation oder abstrakte Datentypentheorie bezeichnet wird. Gegenstand des Studiums 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 gegebenen Sammlung von (Konstruktor-) Operationen generiert, und aus diesem Grund spielt die "Initialität" von Algebren eine so wichtige Rolle.
Algebraische Standardtechniken haben sich bei der Erfassung verschiedener wesentlicher Aspekte von Datenstrukturen in der Informatik als nützlich erwiesen. 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 Staatsbegriff, 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 nach und nach die Einsicht, dass solche staatsbasierten Systeme nicht als Algebren, sondern als sogenannte Co-Algebren bezeichnet werden sollten. Dies ist das formale Dual der Algebren, wie in diesem Tutorial noch genauer erläutert wird. Die doppelte Eigenschaft der "Initialität" für Algebren, nämlich die Finalität, erwies sich als entscheidend für solche Co-Algebren. Und das logische Argumentationsprinzip, das für solche endgültigen Co-Algebren benötigt wird, ist nicht Induktion, sondern Co-Induktion.


Vorspiel zur Kategorietheorie Die Kategorietheorie sollte in Funktortheorie umbenannt werden. Als Kategorien muss man definieren, um Funktoren zu definieren. (Außerdem müssen Funktoren definiert werden, um natürliche Transformationen zu definieren.)

Was ist ein Funktor? Es ist eine Umwandlung von einer Menge in eine andere, die ihre Struktur beibehält. (Für mehr Details gibt es viele gute Beschreibungen im Netz).

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

Wie kann es mit der Informatik verknüpft werden? Das Programm kann als strukturierter Informationssatz betrachtet werden. Die Programmausführung entspricht der Änderung dieses strukturierten Informationssatzes. Es hört sich gut an, dass bei der Ausführung die Programmstruktur erhalten bleibt. Die Ausführung kann dann als Anwendung eines Funktors über diesen Informationssatz betrachtet werden. (Derjenige, der das Programm definiert).

Warum F-Co-Algebra? Programme sind im Wesentlichen dual, da sie durch Informationen beschrieben werden und auf diese einwirken. 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 vom Programm gemeinsam genutzten Informationen definiert werden kann.

Dann möchte ich an dieser Stelle sagen,

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

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

37
zurgl

Ich beginne mit Sachen, die offensichtlich mit Programmierung zu tun haben, und füge dann einige mathematische Sachen hinzu, um sie so konkret und bodenständig wie möglich zu halten.


Zitieren wir einige Informatiker zur Koinduktion…

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 π halten, da er nur eine begrenzte Menge an Speicher hat! Aber was es tun kann, ist ein endliches Programm zu halten, das eine beliebig lange Expansion von π erzeugt, die Sie sich 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 ausgeben. In einigen Sprachen wird jedoch jedes Argument für eine Funktion evaluiert, bevor es an eine Funktion übergeben wird (strikte, nicht faule Evaluierung). Wenn wir diese Verkleinerungsreihenfolge verwenden, wird unser obiges Programm für immer ausgeführt und berechnet die Ziffern von pi, bevor es an unsere Druckerfunktion übergeben werden kann (was niemals geschieht). Da die Maschine nicht unendlich viel Speicher hat, wird das Programm irgendwann keinen Speicher mehr haben und abstürzen. 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 Programmteilen. Eine ähnliche Bequemlichkeit ohne unendlich träge Strukturen zu erreichen, würde in vielen Fällen akrobatische Umkehrungen des Kontrollflusses erfordern.

http://www.alexandrasilva.org/#/talks.html examples of coalgebras by Alexandra Silva


Beziehen des umgebenden mathematischen Kontexts auf übliche Programmieraufgaben

Was ist "eine Algebra"?

Algebraische Strukturen sehen im Allgemeinen so aus:

  1. Stuff
  2. Was das Zeug kann

Dies sollte wie Objekte mit 1. Eigenschaften und 2. Methoden klingen. Oder noch besser, es sollte sich wie eine Typensignatur anhören.

Standardmathematische Beispiele sind Monoid oid Gruppe ⊃ Vektorraum ⊃ "eine Algebra". Monoide sind wie Automaten: Folgen von Verben (zB f.g.h.h.nothing.f.g.f). Ein git -Protokoll, das immer den Verlauf hinzufügt und niemals löscht, wäre ein Monoid, aber keine Gruppe. Wenn Sie Umkehrungen hinzufügen (z. B. negative Zahlen, Brüche, Wurzeln, Löschen des akkumulierten Verlaufs, Zerstören eines zerbrochenen Spiegels), erhalten Sie eine Gruppe.

Gruppen enthalten Dinge, die zusammen addiert oder subtrahiert werden können. Zum Beispiel können Durations zusammenaddiert werden. (Aber Dates können nicht.) Dauern leben in einem Vektorraum (nicht nur in einer Gruppe), weil sie auch durch äußere Zahlen skaliert werden können. (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 Integers verlassen, ist es weniger offensichtlich, was "Multiplikation" (oder "Exponentiation" ) sein soll.

(Deshalb achten die Menschen auf (kategorietheoretische) universelle Eigenschaften: um ihnen zu sagen, was die Multiplikation tun soll oder sei wie :

universal property of product )


Algebren → Kohlengebren

Die Komultiplikation ist einfacher zu definieren als die Multiplikation, da Sie von T → (T,T) aus 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 es wiederum darauf ankommt, was Ihr Counit tut ; trace ist nur eine gute Antwort für Matrizen.

Der Grund, sich ein duales Leerzeichen anzusehen, ist im Allgemeinen, dass es einfacher ist, in diesem Leerzeichen zu denken. Zum Beispiel ist es manchmal einfacher, an einen normalen Vektor zu denken als an die Ebene, zu der er normal ist, aber Sie können Ebenen (einschließlich Hyperebenen) mit Vektoren steuern (und jetzt spreche ich von dem vertrauten geometrischen Vektor, wie in einem Ray-Tracer). .


Zähmung (un) strukturierter Daten

Mathematiker modellieren vielleicht etwas, das Spaß macht, wie TQFTs , während sich Programmierer damit herumschlagen müssen

  • datum/Uhrzeit (+ :: (Date,Duration) → Date),
  • orte (Paris(+48.8567,+2.3508)! Es ist eine Form, kein Punkt.),
  • unstrukturiertes JSON, das in gewissem Sinne konsistent sein soll,
  • falsch-aber-nahes XML,
  • unglaublich komplexe GIS-Daten, die jede Menge sinnvoller Beziehungen befriedigen sollten,
  • reguläre Ausdrücke, die Ihnen etwas bedeuteten , Perl jedoch erheblich weniger bedeuteten.
  • CRM, das alle Telefonnummern und Standorte der Geschäftsleitung, die Namen seiner (jetzt Ex-) Frau und seiner Kinder, den Geburtstag und alle vorherigen Geschenke enthalten sollte. Jedes sollte "offensichtliche" (für den Kunden offensichtliche) Beziehungen befriedigen, die unglaublich sind schwer zu kodieren,
  • .....

Wenn Informatiker über Kohlegebren sprechen, denken sie normalerweise an Set-ish-Operationen wie das kartesische Produkt. Ich glaube, das meinen die Leute mit "Algebren sind Kohlegebren in Haskell". Aber in dem Maße, in dem Programmierer komplexe Datentypen wie Place, Date/Time Und Customer modellieren müssen - und diese Modelle so aussehen lassen, wie sie der realen Welt (oder der Realität) ähneln zumindest die Sichtweise des Endbenutzers auf die reale Welt) als möglich - ich glaube, Duals könnten nicht nur für die Set-Welt nützlich sein.

4
isomorphismes