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 Nilas () -> 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ù 1est un ensemble d'unités (ensemble avec un élément) et l' A × Bopération est un produit croisé de deux ensembles Aet B(c'est-à-dire un ensemble de paires (a, b)où apasse par tous les éléments de Aet bpasse par tous les éléments de B).
Union disjointe de deux ensembles Aet Best un ensemble A | Bqui 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 Aet B, mais avec chacun de ces éléments `` marqués '' comme appartenant à l'un Aou à l'autre B, donc lorsque nous choisirons un élément de, A | Bnous saurons immédiatement si cet élément est venu de Aou de B.
Nous pouvons «joindre» Nilet Consfonctionner, 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|Consfonction est appliquée à la ()valeur (qui, évidemment, appartient à 1 | (Int × IntList)set), alors elle se comporte comme si elle l'était Nil; si Nil|Consest 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 joinedfonctions ont un type similaire: elles ressemblent toutes les deux
f :: F T -> T
où Fest une sorte de transformation qui prend notre type et donne un type plus complexe, qui se compose de xet d' |opérations, d'usages Tet éventuellement d'autres types. Par exemple, pour IntListet IntTree Fressemble à 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ù Test un type et fest 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 ffonction est le même pour chaque F, Tet feux - mêmes peuvent être arbitraires. Par exemple, (String, g :: 1 | (Int x String) -> String)ou (Double, h :: Int | (Double, Double) -> Double)pour certains get hsont é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 foldfonction 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 foldrfonction:
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 foldrfonction 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é foldraura 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 IntListtype.
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 Nilpartie IntList, et la seconde partie définit le comportement de la fonction en Conspartie.
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 btypes 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 reductorc'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 IntLists'agit d'une algèbre F1 initiale, pour chaque type Tet pour chaque fonction, r :: F1 T -> Til existe une fonction, appelée catamorphisme pour r, qui se convertit IntListen T, et cette fonction est unique. En effet, dans notre exemple un catamorphisme pour reductorest sumFold. Notez comment reductoret sumFoldsont similaires: ils ont presque la même structure! L' utilisation en paramètre de reductordéfinition s(dont le type correspond à T) correspond à l'utilisation du résultat du calcul de sumFold xsen sumFolddé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 appendfonction 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
appendFoldest un catamorphisme pour appendReductorlequel se transforme IntListen 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 unfoldspour 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 IntStreams:
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 IntStreamtype. 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ù Test un type. Désormais nous définirons
F1 T = Int × T
Maintenant, F-coalgebra est une paire (T, g), où Test un type et gest 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, get Tpeut ê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, IntStreamest un terminal F-coalgebra. Cela signifie que pour chaque type Tet pour chaque fonction, p :: T -> F1 Til existe une fonction, appelée anamorphisme , qui se transforme Ten 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 natset natsBuilder. Il est très similaire à la connexion que nous avons observée précédemment avec les réducteurs et les plis. natsest 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 iterateun 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.