J'ai entendu le terme "coalgebras" à plusieurs reprises dans les cercles de programmation fonctionnelle et PLT, en particulier lorsqu'il est question d'objets, de comonads, de lentilles, etc. Googler ce terme donne des pages qui donnent une description mathématique de ces structures, ce qui m'est incompréhensible. Quelqu'un peut-il expliquer ce que signifient les coalgebras dans le contexte de la programmation, quelle est leur signification et leur rapport avec les objets et les comonads?
Je pense que le point de départ serait de comprendre l’idée d’une algèbre . Ceci est juste une généralisation de structures algébriques telles que des groupes, des anneaux, des monoïdes, etc. La plupart du temps, ces choses sont introduites en termes de sets, mais comme nous sommes entre amis, je parlerai plutôt des types Haskell. (Je ne peux pas m'empêcher d'utiliser des lettres grecques, elles ont l'air plus cool!)
Une algèbre n'est donc qu'un type τ
Avec des fonctions et des identités. Ces fonctions prennent des nombres différents d'arguments de type τ
Et produisent un τ
: Non pressé, elles ressemblent toutes à (τ, τ,…, τ) → τ
. Ils peuvent aussi avoir des "identités" - des éléments de τ
Qui ont un comportement spécial avec certaines des fonctions.
L'exemple le plus simple est le monoïde. Un monoïde est n'importe quel type τ
Avec une fonction mappend ∷ (τ, τ) → τ
et une identité mzero ∷ τ
. D'autres exemples incluent des groupes tels que des groupes (qui ressemblent à des monoïdes sauf avec une fonction supplémentaire invert ∷ τ → τ
), Des anneaux, des réseaux, etc.
Toutes les fonctions fonctionnent sur τ
Mais peuvent avoir des arités différentes. Nous pouvons les écrire sous la forme τⁿ → τ
, Où τⁿ
Correspond à un tuple de n
τ
. De cette façon, il est logique de penser aux identités en tant que τ⁰ → τ
Où τ⁰
Est simplement le Tuple vide ()
. Nous pouvons donc simplifier l’idée d’une algèbre maintenant: c’est juste un type avec un certain nombre de fonctions.
Une algèbre est juste un motif commun en mathématiques qui a été "factorisé", comme nous le faisons avec du code. Les gens ont remarqué que toute une série de choses intéressantes - les monoïdes, les groupes, les treillis, etc. - mentionnés ci-dessus, suivent toutes un modèle similaire et ont donc été abstraites. L’avantage de faire cela est le même que dans la programmation: cela crée des preuves réutilisables et facilite certains types de raisonnement.
Cependant, nous n'en avons pas encore fini avec l'affacturage. Jusqu'à présent, nous avons un tas de fonctions τⁿ → τ
. Nous pouvons réellement faire une astuce pour les combiner en une seule fonction. En particulier, regardons les monoïdes: nous avons mappend ∷ (τ, τ) → τ
et mempty ∷ () → τ
. Nous pouvons les transformer en une seule fonction en utilisant un type somme - Either
. Cela ressemblerait à ceci:
op ∷ Monoid τ ⇒ Either (τ, τ) () → τ
op (Left (a, b)) = mappend (a, b)
op (Right ()) = mempty
Nous pouvons en fait utiliser cette transformation à plusieurs reprises pour combiner les fonctions toutesτⁿ → τ
En une seule, pour toute algèbre. (En fait, nous pouvons le faire pour n’importe quel nombre de fonctions a → τ
, b → τ
Et ainsi de suite pour toutea, b,…
.)
Cela nous permet de parler des algèbres en tant que type τ
Avec une fonction single à partir d’un désordre de Either
s en un seul τ
. Pour les monoïdes, ce désordre est: Either (τ, τ) ()
; pour les groupes (qui ont une opération supplémentaire τ → τ
), c'est: Either (Either (τ, τ) τ) ()
. C'est un type différent pour chaque structure différente. Alors qu'est-ce que tous ces types ont en commun? La chose la plus évidente est qu’ils ne représentent que des sommes de produits - des types de données algébriques. Par exemple, pour les monoïdes, nous pourrions créer un type d'argument monoïde qui fonctionne pour any monoïde τ:
data MonoidArgument τ = Mappend τ τ -- here τ τ is the same as (τ, τ)
| Mempty -- here we can just leave the () out
Nous pouvons faire la même chose pour les groupes, les anneaux et les treillis et toutes les autres structures possibles.
Quoi d'autre est spécial à propos de tous ces types? Eh bien, ils sont tous Functors
! Par exemple.:
instance Functor MonoidArgument where
fmap f (Mappend τ τ) = Mappend (f τ) (f τ)
fmap f Mempty = Mempty
Nous pouvons donc généraliser encore plus notre idée d’une algèbre. C'est juste un type τ
Avec une fonction f τ → τ
Pour un foncteur f
. En fait, nous pourrions écrire ceci en tant que classe:
class Functor f ⇒ Algebra f τ where
op ∷ f τ → τ
Ceci est souvent appelé une "F-algèbre" car elle est déterminée par le foncteur F
. Si nous pouvions appliquer partiellement les classes de types, nous pourrions définir quelque chose comme class Monoid = Algebra MonoidArgument
.
Nous espérons que vous comprenez bien ce qu'est une algèbre et comment il ne s'agit que d'une généralisation des structures algébriques normales. Alors, qu'est-ce qu'un F-Coalgebra? Eh bien, le co implique que c'est le "double" d'une algèbre - c'est-à-dire, nous prenons une algèbre et retournons quelques flèches. Je ne vois qu'une flèche dans la définition ci-dessus, alors je vais simplement inverser la phrase suivante:
class Functor f ⇒ CoAlgebra f τ where
coop ∷ τ → f τ
Et c'est tout ce que c'est! Maintenant, cette conclusion peut sembler un peu désinvolte (hé). Cela vous dit quoi un charbon, mais ne vous donne pas vraiment une idée de son utilité ou de la raison pour laquelle nous nous en soucions. J'y reviendrai dans un instant, une fois que je trouverai ou trouverai un bon exemple ou deux: P.
Après avoir lu quelques mots, je pense avoir une bonne idée de l’utilisation des coalgebras pour représenter des classes et des objets. Nous avons un type C
qui contient tous les états internes possibles des objets de la classe; la classe elle-même est une charnière sur C
qui spécifie les méthodes et les propriétés des objets.
Comme le montre l'exemple d'algèbre, si nous avons un ensemble de fonctions telles que a → τ
Et b → τ
Pour tout a, b,…
, Nous pouvons toutes les combiner en une seule fonction à l'aide de Either
, un type de somme. La double "notion" serait une combinaison de fonctions de type τ → a
, τ → b
, Etc. Nous pouvons le faire en utilisant le double d'un type de somme - un type de produit. Donc, étant donné les deux fonctions ci-dessus (appelées f
et g
), nous pouvons en créer une unique comme ceci:
both ∷ τ → (a, b)
both x = (f x, g x)
Le type (a, a)
Est un foncteur au sens propre du terme, il correspond donc parfaitement à notre notion de F-coalgebra. Cette astuce nous permet de regrouper un tas de fonctions différentes - ou, pour la POO, de méthodes - en une seule fonction de type τ → f τ
.
Les éléments de notre type C
représentent l'état interne de l'objet. Si l'objet possède des propriétés lisibles, elles doivent pouvoir dépendre de l'état. La manière la plus évidente de le faire est de les transformer en une fonction de C
. Donc, si nous voulons une propriété de longueur (par exemple, object.length
), Nous aurions une fonction C → Int
.
Nous voulons des méthodes pouvant prendre un argument et modifier un état. Pour ce faire, nous devons prendre tous les arguments et produire un nouveau C
. Imaginons une méthode setPosition
qui prend une coordonnée x
et une y
: object.setPosition(1, 2)
. Cela ressemblerait à ceci: C → ((Int, Int) → C)
.
Le modèle important ici est que les "méthodes" et les "propriétés" de l'objet prennent l'objet lui-même comme premier argument. C'est comme le paramètre self
dans Python et comme l'implicite this
de nombreux autres langages. Une coalgèbre ne fait qu'encapsuler le comportement de la prise de self
: c'est le premier C
dans C → F C
.
Alors mettons tout cela ensemble. Imaginons une classe avec une propriété position
, une propriété name
et la fonction setPosition
:
class C
private
x, y : Int
_name : String
public
name : String
position : (Int, Int)
setPosition : (Int, Int) → C
Nous avons besoin de deux parties pour représenter cette classe. Premièrement, nous devons représenter l'état interne de l'objet. dans ce cas, il ne contient que deux Int
et un String
. (Ceci est notre type C
.) Ensuite, nous devons trouver le Coalgebra représentant la classe.
data C = Obj { x, y ∷ Int
, _name ∷ String }
Nous avons deux propriétés à écrire. Ils sont assez triviaux:
position ∷ C → (Int, Int)
position self = (x self, y self)
name ∷ C → String
name self = _name self
Il ne reste plus qu’à pouvoir mettre à jour la position:
setPosition ∷ C → (Int, Int) → C
setPosition self (newX, newY) = self { x = newX, y = newY }
C'est comme une classe Python avec ses variables explicites self
. Maintenant que nous avons un tas de fonctions self →
, Nous devons les combiner en une seule fonctionner pour le charbon, nous pouvons le faire avec un simple tuple:
coop ∷ C → ((Int, Int), String, (Int, Int) → C)
coop self = (position self, name self, setPosition self)
Le type ((Int, Int), String, (Int, Int) → c)
- for anyc
- est un foncteur, donc coop
a la forme que nous voulons: Functor f ⇒ C → f C
.
Compte tenu de cela, C
avec coop
forment un coalgebra qui spécifie la classe que j'ai donnée ci-dessus. Vous pouvez voir comment nous pouvons utiliser cette même technique pour spécifier un nombre quelconque de méthodes et de propriétés pour nos objets.
Cela nous permet d’utiliser le raisonnement charbon-cbrique pour traiter les classes. Par exemple, nous pouvons introduire la notion d'un "homomorphisme F-Coalgebra" pour représenter les transformations entre classes. C’est un terme qui a l’air effrayant qui signifie simplement une transformation entre coalgebras qui préserve la structure. Cela facilite beaucoup la réflexion sur le mappage de classes sur d’autres classes.
En bref, un F-coalgebra représente une classe en disposant d'un ensemble de propriétés et de méthodes qui dépendent toutes d'un paramètre self
contenant l'état interne de chaque objet.
Jusqu'ici, nous avons parlé des algèbres et des coalgebras en tant que types de Haskell. Une algèbre est juste un type τ
Avec une fonction f τ → τ
Et une coalge est juste un type τ
Avec une fonction τ → f τ
.
Cependant, rien n'attache vraiment ces idées à Haskell en soi. En fait, ils sont généralement introduits en termes d'ensembles et de fonctions mathématiques plutôt qu'en termes de types et de fonctions Haskell. En effet, nous pouvons généraliser ces concepts aux catégories any!
Nous pouvons définir une F-algèbre pour une catégorie C
. Premièrement, nous avons besoin d’un foncteur F : C → C
- c’est-à-dire un endofunctor. (Tous les Haskell Functor
sont en réalité des endofoncteurs à partir de Hask → Hask
.) Ensuite, une algèbre n'est qu'un objet A
de C
avec un morphisme F A → A
. Un charbon est le même sauf avec A → F A
.
Que gagne-t-on en considérant d'autres catégories? Nous pouvons utiliser les mêmes idées dans différents contextes. Comme des monades. En Haskell, une monade est du type M ∷ ★ → ★
Avec trois opérations:
map ∷ (α → β) → (M α → M β)
return ∷ α → M α
join ∷ M (M α) → M α
La fonction map
est juste une preuve du fait que M
est un Functor
. On peut donc dire qu’une monade n’est qu’un foncteur avec deux opérations: return
et join
.
Les foncteurs forment eux-mêmes une catégorie, avec des morphismes entre eux appelés "transformations naturelles". Une transformation naturelle n’est qu’un moyen de transformer un foncteur en un autre tout en préservant sa structure. Voici un bel article aidant à expliquer l’idée. Il parle de concat
, qui est juste join
pour les listes.
Avec les foncteurs Haskell, la composition de deux foncteurs est un foncteur lui-même. En pseudocode, nous pourrions écrire ceci:
instance (Functor f, Functor g) ⇒ Functor (f ∘ g) where
fmap fun x = fmap (fmap fun) x
Cela nous aide à penser à join
comme un mappage de f ∘ f → f
. Le type de join
est ∀α. f (f α) → f α
. Intuitivement, nous pouvons voir comment une fonction valide pour tous types α
Peut être considérée comme une transformation de f
.
return
est une transformation similaire. Son type est ∀α. α → f α
. Cela a l'air différent: le premier α
N'est pas "dans" un foncteur! Heureusement, nous pouvons résoudre ce problème en y ajoutant un foncteur d’identité: ∀α. Identity α → f α
. Donc return
est une transformation Identity → f
.
Nous pouvons maintenant considérer une monade comme une simple algèbre basée sur un foncteur f
avec les opérations f ∘ f → f
Et Identity → f
. Cela ne vous semble-t-il pas familier? C'est très similaire à un monoïde, qui était juste un type τ
Avec les opérations τ × τ → τ
Et () → τ
.
Donc, une monade est comme un monoïde, sauf qu'au lieu d'avoir un type, nous avons un foncteur. C'est le même genre d'algèbre, juste dans une catégorie différente. (C'est là que la phrase "Une monade est juste un monoïde dans la catégorie des endofoncteurs" vient d'aussi loin que je sache.)
Maintenant, nous avons ces deux opérations: f ∘ f → f
Et Identity → f
. Pour obtenir le charbon correspondant, il suffit de retourner les flèches. Cela nous donne deux nouvelles opérations: f → f ∘ f
Et f → Identity
. Nous pouvons les transformer en types Haskell en ajoutant les variables de type comme ci-dessus, en nous donnant ∀α. f α → f (f α)
et ∀α. f α → α
. Cela ressemble à la définition d'un comonad:
class Functor f ⇒ Comonad f where
coreturn ∷ f α → α
cojoin ∷ f α → f (f α)
Ainsi, un comonad est alors un coalgebra dans une catégorie d’endofoncteurs.
Les F-algèbres et les F-coalgèbres sont des structures mathématiques qui permettent de raisonner sur types inductifs (ou types récursifs).
Nous commencerons par les F-algèbres. Je vais essayer d'être aussi simple que possible.
Je suppose que vous savez ce qui est un type récursif. Par exemple, il s'agit d'un type pour une liste d'entiers:
data IntList = Nil | Cons (Int, IntList)
Il est évident qu’il est récursif - sa définition fait référence à elle-même. Sa définition consiste en deux constructeurs de données, qui ont les types suivants:
Nil :: () -> IntList
Cons :: (Int, IntList) -> IntList
Notez que j'ai écrit le type de Nil
comme () -> IntList
, Pas simplement IntList
. Ce sont en fait des types équivalents du point de vue théorique, car le type ()
N'a qu'un seul habitant.
Si nous écrivons les signatures de ces fonctions d’une manière plus théorique, nous aurons
Nil :: 1 -> IntList
Cons :: Int × IntList -> IntList
où 1
est un ensemble d'unités (défini avec un élément) et l'opération A × B
est un produit croisé de deux ensembles A
et B
(c'est-à-dire de paires (a, b)
où a
passe par tous les éléments de A
et b
passe par tous les éléments de B
).
L'union disjointe de deux ensembles A
et B
est un ensemble A | B
Qui est une union d'ensembles {(a, 1) : a in A}
et {(b, 2) : b in B}
. Il s'agit essentiellement d'un ensemble de tous les éléments de A
et de B
, mais chacun de ces éléments est "marqué" comme appartenant à A
ou à B
. Ainsi, lorsque nous choisirons un élément de A | B
, nous saurons immédiatement si cet élément provient de A
ou de B
.
Nous pouvons "joindre" des fonctions Nil
et Cons
, de sorte qu'elles ne formeront qu'une seule fonction travaillant sur un ensemble 1 | (Int × IntList)
:
Nil|Cons :: 1 | (Int × IntList) -> IntList
En effet, si la fonction Nil|Cons
Est appliquée à la valeur ()
(Qui, évidemment, appartient à 1 | (Int × IntList)
set), elle se comporte alors comme si elle était Nil
; si Nil|Cons
est appliqué à une valeur de type (Int, IntList)
(ces valeurs figurent également dans l'ensemble 1 | (Int × IntList)
, il se comporte comme Cons
.
Considérons maintenant un autre type de données:
data IntTree = Leaf Int | Branch (IntTree, IntTree)
Il a les constructeurs suivants:
Leaf :: Int -> IntTree
Branch :: (IntTree, IntTree) -> IntTree
qui peut également être réuni dans une fonction:
Leaf|Branch :: Int | (IntTree × IntTree) -> IntTree
On peut voir que les deux fonctions joined
ont un type similaire: elles ressemblent toutes les deux
f :: F T -> T
où F
est une sorte de transformation qui prend notre type et donne un type plus complexe, qui comprend les opérations x
et |
, les utilisations de T
et éventuellement autres types. Par exemple, pour IntList
et IntTree
, F
se présente comme suit:
F1 T = 1 | (Int × T)
F2 T = Int | (T × T)
Nous pouvons immédiatement remarquer que tout type algébrique peut être écrit de cette façon. C’est pourquoi on les appelle "algébriques": elles consistent en un certain nombre de "sommes" (unions) et de "produits" (produits croisés) d’autres types.
Nous pouvons maintenant définir F-algebra. F-Algèbre est juste une paire (T, f)
, Où T
est un type et f
est une fonction de type f :: F T -> T
. Dans nos exemples, les algèbres F sont (IntList, Nil|Cons)
Et (IntTree, Leaf|Branch)
. Notez cependant que malgré ce type de fonction f
est identique pour chaque F, T
et f
eux-mêmes peuvent être arbitraires. Par exemple, (String, g :: 1 | (Int x String) -> String)
Ou (Double, h :: Int | (Double, Double) -> Double)
Pour certains g
et h
sont également des F-algèbres pour le F. correspondant.
Ensuite, nous pouvons introduire homomorphismes de F-algèbre et ensuite F-algèbres initiales, qui ont des propriétés très utiles. En fait, (IntList, Nil|Cons)
Est une F1-algèbre initiale et (IntTree, Leaf|Branch)
Est une F2-algèbre initiale. Je ne présenterai pas de définitions exactes de ces termes et propriétés car ils sont plus complexes et abstraits que nécessaire.
Néanmoins, le fait que, par exemple, (IntList, Nil|Cons)
Soit F-algèbre nous permet de définir une fonction de type fold
sur ce type. Comme vous le savez, fold est une sorte d'opération qui transforme un type de données récursif en une valeur finie. Par exemple, nous pouvons plier une liste d'entiers en une seule valeur qui est la somme de tous les éléments de la liste:
foldr (+) 0 [1, 2, 3, 4] -> 1 + 2 + 3 + 4 = 10
Il est possible de généraliser une telle opération sur n'importe quel type de données récursif.
Ce qui suit est une signature de la fonction foldr
:
foldr :: ((a -> b -> b), b) -> [a] -> b
Notez que j'ai utilisé des accolades pour séparer les deux premiers arguments du dernier. Ce n'est pas une vraie fonction foldr
, mais elle est isomorphe (c'est-à-dire que vous pouvez facilement en obtenir une de l'autre et vice versa). Appliqué partiellement foldr
aura la signature suivante:
foldr ((+), 0) :: [Int] -> Int
Nous pouvons voir qu'il s'agit d'une fonction qui prend une liste d'entiers et renvoie un seul entier. Définissons cette fonction en fonction de notre type IntList
.
sumFold :: IntList -> Int
sumFold Nil = 0
sumFold (Cons x xs) = x + sumFold xs
Nous voyons que cette fonction est composée de deux parties: la première partie définit le comportement de cette fonction sur la partie Nil
de IntList
et la deuxième partie définit le comportement de la fonction sur la partie Cons
.
Supposons maintenant que nous ne programmons pas en Haskell, mais dans un langage qui autorise l’utilisation de types algébriques directement dans les signatures de types (eh bien, techniquement, Haskell autorise l’utilisation de types algébriques via des tuples et le type de données Either a b
, Mais cela entraînera des modifications inutiles. verbosité). Considérons une fonction:
reductor :: () | (Int × Int) -> Int
reductor () = 0
reductor (x, s) = x + s
On peut voir que reductor
est une fonction de type F1 Int -> Int
, Tout comme dans la définition de F-algebra! En effet, la paire (Int, reductor)
Est une algèbre F1.
Parce que IntList
est une algèbre F1 initiale, pour chaque type T
et pour chaque fonction r :: F1 T -> T
, Il existe une fonction, appelée catamorphisme pour r
, qui convertit IntList
en T
, et cette fonction est unique. En effet, dans notre exemple, un catamorphisme pour reductor
est sumFold
. Notez que reductor
et sumFold
sont similaires: ils ont presque la même structure! Dans reductor
, l'utilisation du paramètre s
(dont le type correspond à T
) correspond à l'utilisation du résultat du calcul de sumFold xs
Dans sumFold
définition.
Juste pour que ce soit plus clair et pour vous aider à voir le motif, voici un autre exemple, et nous commençons encore une fois par la fonction de pliage résultante. Considérons la fonction append
qui ajoute son premier argument au second:
(append [4, 5, 6]) [1, 2, 3] = (foldr (:) [4, 5, 6]) [1, 2, 3] -> [1, 2, 3, 4, 5, 6]
Voici à quoi ça ressemble sur notre IntList
:
appendFold :: IntList -> IntList -> IntList
appendFold ys () = ys
appendFold ys (Cons x xs) = x : appendFold ys xs
Encore une fois, essayons d'écrire le réducteur:
appendReductor :: IntList -> () | (Int × IntList) -> IntList
appendReductor ys () = ys
appendReductor ys (x, rs) = x : rs
appendFold
est un catamorphisme pour appendReductor
qui transforme IntList
en IntList
.
Les algèbres F nous permettent donc de définir des "replis" sur des infrastructures de données récursives, c'est-à-dire des opérations qui réduisent nos structures à une certaine valeur.
Les F-Coalgebras sont ce qu'on appelle le "double" terme pour F-Algèbres Ils nous permettent de définir unfolds
pour les types de données récursifs, c’est-à-dire un moyen de construire des structures récursives à partir de certaines valeurs.
Supposons que vous ayez le type suivant:
data IntStream = Cons (Int, IntStream)
C'est un flux infini d'entiers. Son seul constructeur a le type suivant:
Cons :: (Int, IntStream) -> IntStream
Ou, en termes d'ensembles
Cons :: Int × IntStream -> IntStream
Haskell vous permet de rechercher des modèles sur les constructeurs de données. Vous pouvez donc définir les fonctions suivantes fonctionnant sur les IntStream
:
head :: IntStream -> Int
head (Cons (x, xs)) = x
tail :: IntStream -> IntStream
tail (Cons (x, xs)) = xs
Vous pouvez naturellement "joindre" ces fonctions en une seule fonction de type IntStream -> Int × IntStream
:
head&tail :: IntStream -> Int × IntStream
head&tail (Cons (x, xs)) = (x, xs)
Remarquez comment le résultat de la fonction coïncide avec la représentation algébrique de notre type IntStream
. Une action similaire peut également être effectuée pour d'autres types de données récursifs. Peut-être avez-vous déjà remarqué le motif. Je parle d'une famille de fonctions de type
g :: T -> F T
où T
est un type. A partir de maintenant nous définirons
F1 T = Int × T
Maintenant, F-coalgebra est une paire (T, g)
, Où T
est un type et g
est une fonction de type g :: T -> F T
. Par exemple, (IntStream, head&tail)
Est un F1-Coalgebra. Encore une fois, comme dans les F-algèbres, g
et T
peuvent être arbitraires, par exemple, (String, h :: String -> Int x String)
Est également un facteur F1 pour certains h.
Parmi tous les F-coalgebras, il y a ce qu'on appelle terminaux F-coalgebras, qui sont duels aux F-algèbres initiales. Par exemple, IntStream
est un terminal F-coalgebra. Cela signifie que pour chaque type T
et pour chaque fonction p :: T -> F1 T
, Il existe une fonction, appelée anamorphisme, qui convertit T
en IntStream
, et cette fonction est unique.
Considérons la fonction suivante, qui génère un flux d'entiers successifs à partir de celui donné:
nats :: Int -> IntStream
nats n = Cons (n, nats (n+1))
Inspectons maintenant une fonction natsBuilder :: Int -> F1 Int
, C'est-à-dire natsBuilder :: Int -> Int × Int
:
natsBuilder :: Int -> Int × Int
natsBuilder n = (n, n+1)
Encore une fois, nous pouvons voir une certaine similarité entre nats
et natsBuilder
. Cela ressemble beaucoup à la connexion observée précédemment avec les réducteurs et les plis. nats
est un anamorphisme pour natsBuilder
.
Autre exemple, une fonction qui prend une valeur et une fonction et renvoie un flux d’applications successives de la fonction à la valeur:
iterate :: (Int -> Int) -> Int -> IntStream
iterate f n = Cons (n, iterate f (f n))
Sa fonction constructeur est la suivante:
iterateBuilder :: (Int -> Int) -> Int -> Int × Int
iterateBuilder f n = (n, f n)
Alors iterate
est un anamorphisme pour iterateBuilder
.
En bref, les F-algèbres permettent de définir des plis, c’est-à-dire des opérations qui réduisent la structure récursive en une seule valeur, et F-coalgebras permettent de faire le contraire: construire une structure [potentiellement] infinie à partir d’une seule valeur.
En fait, dans Haskell F-algèbres et F-coalgebras coïncident. Ceci est une très belle propriété qui est une conséquence de la présence de valeur "inférieure" dans chaque type. Ainsi, dans Haskell, les plis et les déplis peuvent être créés pour chaque type récursif. Cependant, le modèle théorique derrière cela est plus complexe que celui que j'ai présenté ci-dessus, je l'ai donc délibérément évité.
J'espère que cela t'aides.
En parcourant le didacticiel n didacticiel sur les (co) algèbres et la (co) induction devrait vous donner un aperçu de la co-algèbre en informatique.
Vous trouverez ci-dessous une citation pour vous convaincre,
En termes généraux, un programme dans un langage de programmation manipule des données. Au cours du développement de l'informatique au cours des dernières décennies, il est apparu clairement qu'une description abstraite de ces données était souhaitable, par exemple pour s'assurer que son programme ne dépendait pas de la représentation particulière des données sur lesquelles il opère. En outre, une telle abstraite facilite les preuves de correction.
Ce désir a conduit à l’utilisation de méthodes algébriques en informatique, dans une branche appelée spécification algébrique ou théorie des types de données abstraits. Les objets d'étude sont des types de données en eux-mêmes, utilisant des notions de techniques familières de l'algèbre. Les types de données utilisés par les informaticiens sont souvent générés à partir d'un ensemble donné d'opérations (constructeurs), et c'est pour cette raison que "l'initialité" des algèbres joue un rôle si important.
Les techniques algébriques classiques se sont révélées utiles pour saisir divers aspects essentiels des structures de données utilisées en informatique. Mais il s'est avéré difficile de décrire algébriquement certaines des structures intrinsèquement dynamiques apparaissant en informatique. De telles structures impliquent généralement une notion d'état, qui peut être transformée de différentes manières. Les approches formelles de tels systèmes dynamiques basés sur des états utilisent généralement des automates ou des systèmes de transition, en tant que références classiques classiques.
Au cours de la dernière décennie, on a compris de plus en plus que de tels systèmes étatiques ne devraient pas être décrits comme des algèbres, mais comme des soi-disant co-algèbres. Ce sont les duels formels d'algèbres, d'une manière qui sera précisée dans ce tutoriel. La double propriété de "l'initialité" des algèbres, à savoir la finalité, s'est avérée cruciale pour ces co-algèbres. Et le principe de raisonnement logique nécessaire pour de tels co-algèbres finaux n’est pas une induction, mais une co-induction.
Prélude, à propos de la théorie des catégories. La théorie des catégories devrait être renommer la théorie des foncteurs. Comme les catégories sont ce qu’il faut définir pour définir les foncteurs. (De plus, les foncteurs sont ce qu’il faut définir pour définir les transformations naturelles.)
Qu'est-ce qu'un foncteur? C'est une transformation d'un ensemble à un autre qui préserve leur structure. (Pour plus de détails, il y a beaucoup de bonnes descriptions sur le net).
Qu'est-ce qu'une algèbre F? C'est l'algèbre du foncteur. C'est juste l'étude de la propriété universelle du foncteur.
Comment peut-il être lié à la science informatique? Le programme peut être considéré comme un ensemble structuré d'informations. L'exécution du programme correspond à la modification de cet ensemble structuré d'informations. Il semble bon que l’exécution préserve la structure du programme. Ensuite, l’exécution peut être vue comme l’application d’un foncteur sur cet ensemble d’informations. (Celui définissant le programme).
Pourquoi le F-co-algèbre? Les programmes sont doubles par essence car ils sont décrits par des informations et agissent en conséquence. Alors principalement les informations qui composent le programme et les font changer peuvent être vues de deux manières.
Alors à ce stade, je voudrais dire que,
Pendant la vie d'un programme, les données et l'état coexistent et se complètent. Ils sont doubles.
Je vais commencer par des éléments qui sont évidemment liés à la programmation, puis par la suite, afin de rester aussi concret et réaliste que possible.
http://www.cs.umd.edu/~micinski/posts/2012-09-04-on-understanding-coinduction.html
L'induction concerne des données finies, la co-induction concerne des données infinies.
L'exemple typique de données infinies est le type d'une liste paresseuse (un flux). Par exemple, disons que nous avons l'objet suivant en mémoire:
let (pi : int list) = (* some function which computes the digits of
π. *)
L’ordinateur ne peut pas contenir la totalité de π car il ne dispose que d’une quantité de mémoire limitée! Mais ce qu’elle peut faire, c’est maintenir un programme fini, qui produira toute extension arbitrairement longue de π que vous désirez. Tant que vous n'utilisez que des parties finies de la liste, vous pouvez calculer avec cette liste infinie autant que vous avez besoin.
Cependant, considérons le programme suivant:
let print_third_element (k : int list) = match k with
| _ :: _ :: thd :: tl -> print thd
print_third_element pi
Ce programme devrait imprimer le troisième chiffre de pi. Mais dans certaines langues, tout argument d'une fonction est évalué avant d'être passé à une fonction (évaluation stricte et non paresseuse). Si nous utilisons cet ordre de réduction, notre programme ci-dessus fonctionnera à tout jamais en calculant les chiffres de pi avant de pouvoir les transmettre à notre fonction d'impression (ce qui ne se produit jamais). Puisque la machine n'a pas de mémoire infinie, le programme finira par manquer de mémoire et tombera en panne. Cela pourrait ne pas être le meilleur ordre d'évaluation.
http://adam.chlipala.net/cpdt/html/Coinductive.html
Dans des langages de programmation fonctionnels comme Haskell, des structures de données infinies sont omniprésentes. Des listes infinies et des types de données plus exotiques fournissent des abstractions pratiques pour la communication entre les parties d'un programme. Obtenir la même commodité sans des structures paresseuses infinies exigerait, dans de nombreux cas, des inversions acrobatiques du flux de contrôle.
http://www.alexandrasilva.org/#/talks.html
Les structures algébriques ressemblent généralement à:
Cela devrait ressembler à des objets avec 1. des propriétés et 2. des méthodes. Ou mieux encore, cela devrait ressembler à des signatures de type.
Les exemples mathématiques standard incluent monoïde ⊃ groupe ⊃ espace vectoriel "une algèbre". Les monoïdes sont comme des automates: séquences de verbes (par exemple, f.g.h.h.nothing.f.g.f
). Un journal git
qui ajoute toujours un historique et ne le supprime jamais serait un monoïde mais pas un groupe. Si vous ajoutez des inverses (par exemple des nombres négatifs, des fractions, des racines, en supprimant l'historique accumulé, en brisant un miroir brisé), vous obtenez un groupe.
Les groupes contiennent des éléments qui peuvent être ajoutés ou soustraits ensemble. Par exemple, Duration
s peuvent être additionnés. (Mais Date
ne le peut pas.) Les durées vivent dans un espace vectoriel (pas seulement un groupe) car elles peuvent également être mises à l'échelle par des nombres extérieurs. (Une signature de type de scaling :: (Number,Duration) → Duration
.)
Les algèbres ⊂ espaces vectoriels peuvent faire encore une chose: il y a quelques m :: (T,T) → T
. Appelez cette "multiplication" ou non, car une fois que vous quittez Integers
, il est moins évident de définir la "multiplication" (ou "exponentiation" ).
(C’est pourquoi les gens s’intéressent aux propriétés universelles (théoriques des catégories): pour leur dire ce que la multiplication devrait faire ou être comme :
)
La multiplication est plus facile à définir de manière non arbitraire que la multiplication, car pour passer de T → (T,T)
, vous pouvez simplement répéter le même élément. ("carte diagonale" - comme des matrices/opérateurs diagonaux dans la théorie spectrale)
Counit est généralement la trace (somme des entrées diagonales), bien que ce qui est important, c’est ce que fait votre pays ; trace
n'est qu'une bonne réponse pour les matrices.
La raison de regarder un double espace , en général, c'est parce qu'il est plus facile de penser dans cet espace. Par exemple, il est parfois plus facile de penser à un vecteur normal qu'à un plan normal, mais vous pouvez contrôler les plans (y compris les hyperplans) avec des vecteurs (et maintenant, je parle du vecteur géométrique familier, comme dans un traceur de rayons). .
Les mathématiciens pourraient modéliser quelque chose d'amusant comme TQFT , alors que les programmeurs doivent se débattre avec
+ :: (Date,Duration) → Date
),Paris
≠ (+48.8567,+2.3508)
! C'est une forme, pas un point.),Les informaticiens, lorsqu'ils parlent de coalgebras, ont généralement à l'esprit des opérations sournoises, comme un produit cartésien. Je pense que c’est ce que les gens veulent dire quand ils disent: "Les algèbres sont des coalgebras de Haskell". Mais dans la mesure où les programmeurs doivent modéliser des types de données complexes tels que Place
, Date/Time
Et Customer
- et faire en sorte que ces modèles ressemblent autant au monde réel moins le point de vue de l'utilisateur final sur le monde réel) possible - je crois que les duels, pourraient être utiles au-delà du seul monde défini.