Les algèbres F et les algèbres F sont des structures mathématiques qui contribuent au raisonnement sur les types inductifs (ou récursifs ).
Algèbres F
Nous allons commencer par les algèbres F. J'essaierai d'être aussi simple que possible.
Je suppose que vous savez ce qu'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 - en effet, sa définition se réfère à lui-même. Sa définition se compose de 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
as () -> 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 des signatures de ces fonctions d'une manière plus théorique, nous obtiendrons
Nil :: 1 -> IntList
Cons :: Int × IntList -> IntList
où 1
est un ensemble d'unités (ensemble avec un élément) et l' A × B
opération est un produit croisé de deux ensembles A
et B
(c'est-à-dire un ensemble de paires (a, b)
où a
passe par tous les éléments de A
et b
passe par tous les éléments de B
).
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}
. Essentiellement, il s'agit d'un ensemble de tous les éléments des deux A
et B
, mais avec chacun de ces éléments `` marqués '' comme appartenant à l'un A
ou à l'autre B
, donc lorsque nous choisirons un élément de, A | B
nous saurons immédiatement si cet élément est venu de A
ou de B
.
Nous pouvons «joindre» Nil
et Cons
fonctionner, afin qu'ils forment une seule fonction travaillant sur un ensemble 1 | (Int × IntList)
:
Nil|Cons :: 1 | (Int × IntList) -> IntList
En effet, si la Nil|Cons
fonction est appliquée à la ()
valeur (qui, évidemment, appartient à 1 | (Int × IntList)
set), alors elle se comporte comme si elle l'était Nil
; si Nil|Cons
est appliqué à n'importe quelle valeur de type (Int, IntList)
(ces valeurs sont é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éunie en une seule fonction:
Leaf|Branch :: Int | (IntTree × IntTree) -> IntTree
On peut voir que ces deux joined
fonctions 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 se compose de x
et d' |
opérations, d'usages T
et éventuellement d'autres types. Par exemple, pour IntList
et IntTree
F
ressemble à ceci:
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. En effet, c'est pourquoi ils sont appelés «algébriques»: ils sont constitués d'un certain nombre de «sommes» (unions) et de «produits» (produits croisés) d'autres types.
Nous pouvons maintenant définir l'algèbre F. L'algèbre F 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 f
fonction est le même 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 algèbres F pour le F. correspondant
Ensuite, nous pouvons introduire des homomorphismes d'algèbre F puis des algèbres F initiales , qui ont des propriétés très utiles. En fait, (IntList, Nil|Cons)
est une algèbre F1 initiale, et (IntTree, Leaf|Branch)
est une algèbre F2 initiale. Je ne présenterai pas les 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, disons, (IntList, Nil|Cons)
est l'algèbre F nous permet de définir une fold
fonction semblable à celle de 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 replier une liste d'entiers en une seule valeur qui est une 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 foldr
fonction:
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 foldr
fonction réelle , mais elle lui est isomorphe (c'est-à-dire que vous pouvez facilement obtenir l'une de l'autre et vice versa). Partiellement appliqué foldr
aura la signature suivante:
foldr ((+), 0) :: [Int] -> Int
Nous pouvons voir que c'est une fonction qui prend une liste d'entiers et retourne un seul entier. Définissons une telle fonction en termes de notre IntList
type.
sumFold :: IntList -> Int
sumFold Nil = 0
sumFold (Cons x xs) = x + sumFold xs
Nous voyons que cette fonction se compose de deux parties: la première partie définit le comportement de cette fonction en Nil
partie IntList
, et la seconde partie définit le comportement de la fonction en Cons
partie.
Supposons maintenant que nous programmons non pas en Haskell mais dans un langage qui permet l'utilisation de types algébriques directement dans les signatures de type (enfin, techniquement Haskell permet l'utilisation de types algébriques via des tuples et des Either a b
types de données, mais cela conduira à une verbosité inutile). Considérons une fonction:
reductor :: () | (Int × Int) -> Int
reductor () = 0
reductor (x, s) = x + s
On peut voir que reductor
c'est une fonction de type F1 Int -> Int
, tout comme dans la définition de l'algèbre F! En effet, la paire (Int, reductor)
est une algèbre F1.
Parce qu'il IntList
s'agit d'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 se convertit IntList
en T
, et cette fonction est unique. En effet, dans notre exemple un catamorphisme pour reductor
est sumFold
. Notez comment reductor
et sumFold
sont similaires: ils ont presque la même structure! L' utilisation en paramètre de reductor
définition s
(dont le type correspond à T
) correspond à l'utilisation du résultat du calcul de sumFold xs
en sumFold
définition.
Juste pour le rendre plus clair et vous aider à voir le motif, voici un autre exemple, et nous recommençons à partir de la fonction de pliage résultante. Considérons la append
fonction 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 cela 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
lequel se transforme IntList
en IntList
.
Donc, essentiellement, les algèbres F nous permettent 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.
F-coalgebras
Les algèbres F sont des termes dits «doubles» pour les algèbres F. 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 d'une certaine valeur.
Supposons que vous ayez le type suivant:
data IntStream = Cons (Int, IntStream)
Il s'agit d'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 faire correspondre les modèles sur les constructeurs de données, vous pouvez donc définir les fonctions suivantes en travaillant sur IntStream
s:
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 IntStream
type. Une opération similaire peut également être effectuée pour d'autres types de données récursives. Vous avez peut-être déjà remarqué le motif. Je fais référence à une famille de fonctions de type
g :: T -> F T
où T
est un type. Désormais 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 charbon-F1. Encore une fois, tout comme dans les algèbres F, g
et T
peut être arbitraire, par exemple, (String, h :: String -> Int x String)
est également un charbon-F1 pour quelque h.
Parmi tous les coalgebras F, il existe des soi-disant F-coalgebras terminaux , qui sont duaux des algèbres F 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 se transforme 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 similitude entre nats
et natsBuilder
. Il est très similaire à la connexion que nous avons 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 de générateur est la suivante:
iterateBuilder :: (Int -> Int) -> Int -> Int × Int
iterateBuilder f n = (n, f n)
C'est alors iterate
un anamorphisme pour iterateBuilder
.
Conclusion
Donc, en bref, les algèbres F permettent de définir des plis, c'est-à-dire des opérations qui réduisent la structure récursive en une seule valeur, et les algèbres F permettent de faire le contraire: construire une structure [potentiellement] infinie à partir d'une seule valeur.
En fait, dans Haskell, les F-algèbres et les F-coalgebras coïncident. Il s'agit d'une très belle propriété qui est une conséquence de la présence de la valeur «bas» dans chaque type. Ainsi, dans Haskell, les plis et les dépliages 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, donc je l'ai délibérément évité.
J'espère que cela t'aides.