Le algebre F e le coalgebre F sono strutture matematiche che sono strumentali nel ragionamento sui tipi induttivi (o tipi ricorsivi ).
F-algebre
Inizieremo prima con le F-algebre. Cercherò di essere il più semplice possibile.
Immagino che tu sappia cos'è un tipo ricorsivo. Ad esempio, questo è un tipo per un elenco di numeri interi:
data IntList = Nil | Cons (Int, IntList)
È ovvio che è ricorsivo, anzi, la sua definizione si riferisce a se stessa. La sua definizione è composta da due costruttori di dati, che hanno i seguenti tipi:
Nil :: () -> IntList
Cons :: (Int, IntList) -> IntList
Nota che ho scritto il tipo di Nilas () -> IntList, non semplicemente IntList. Questi sono in realtà tipi equivalenti dal punto di vista teorico, perché il ()tipo ha un solo abitante.
Se scriviamo le firme di queste funzioni in un modo più teorico, otterremo
Nil :: 1 -> IntList
Cons :: Int × IntList -> IntList
dove 1è un insieme di unità (impostato con un elemento) e l' A × Boperazione è un prodotto incrociato di due insiemi Ae B(ovvero, un insieme di coppie in (a, b)cui aattraversa tutti gli elementi Ae battraversa tutti gli elementi di B).
Unione disgiunta di due insiemi Aed Bè un insieme A | Bche è un'unione di insiemi {(a, 1) : a in A}e {(b, 2) : b in B}. Essenzialmente è un insieme di tutti gli elementi di entrambi Ae B, ma con ciascuno di questi elementi 'marcati' come appartenenti a uno Ao B, quindi quando scegliamo qualsiasi elemento da A | Bsapremo immediatamente se questo elemento proviene da Ao da B.
Possiamo "unire" Nile Consfunzioni, quindi formeranno un'unica funzione che lavora su un set 1 | (Int × IntList):
Nil|Cons :: 1 | (Int × IntList) -> IntList
Infatti, se la Nil|Consfunzione viene applicata al ()valore (che, ovviamente, appartiene al 1 | (Int × IntList)set), allora si comporta come se fosse Nil; se Nil|Consviene applicato a qualsiasi valore di tipo (Int, IntList)(tali valori sono anche nell'insieme 1 | (Int × IntList), si comporta come Cons.
Ora considera un altro tipo di dati:
data IntTree = Leaf Int | Branch (IntTree, IntTree)
Ha i seguenti costruttori:
Leaf :: Int -> IntTree
Branch :: (IntTree, IntTree) -> IntTree
che può anche essere unito in una funzione:
Leaf|Branch :: Int | (IntTree × IntTree) -> IntTree
Si può vedere che entrambe queste joinedfunzioni hanno un tipo simile: sembrano entrambe
f :: F T -> T
dove Fc'è un tipo di trasformazione che prende il nostro tipo e dà un tipo più complesso, che consiste di xe |operazioni, usi Te possibilmente altri tipi. Ad esempio, per IntListe si IntTree Fpresenta come segue:
F1 T = 1 | (Int × T)
F2 T = Int | (T × T)
Possiamo immediatamente notare che qualsiasi tipo algebrico può essere scritto in questo modo. In effetti, è per questo che sono chiamati "algebrici": consistono in una serie di "somme" (sindacati) e "prodotti" (prodotti incrociati) di altri tipi.
Ora possiamo definire l'algebra F. L'algebra F è solo una coppia (T, f), in cui Tesiste un tipo ed fè una funzione di tipo f :: F T -> T. Nei nostri esempi F-algebre sono (IntList, Nil|Cons)e (IntTree, Leaf|Branch). Si noti, tuttavia, che nonostante quel tipo di ffunzione è lo stesso per ogni F, Te fpossono essere arbitrari. Ad esempio, (String, g :: 1 | (Int x String) -> String)o (Double, h :: Int | (Double, Double) -> Double)per alcuni ge hsono anche F-algebre per la corrispondente F.
Successivamente possiamo introdurre omomorfismi dell'algebra F e quindi algebre F iniziali , che hanno proprietà molto utili. In effetti, (IntList, Nil|Cons)è un'algebra F1 iniziale ed (IntTree, Leaf|Branch)è un'algebra F2 iniziale. Non presenterò definizioni precise di questi termini e proprietà poiché sono più complessi e astratti del necessario.
Tuttavia, il fatto che, diciamo, (IntList, Nil|Cons)sia l'algebra F ci consente di definire una foldfunzione simile a questo tipo. Come sapete, fold è un tipo di operazione che trasforma alcuni tipi di dati ricorsivi in un valore finito. Ad esempio, possiamo piegare un elenco di numeri interi in un singolo valore che è la somma di tutti gli elementi nell'elenco:
foldr (+) 0 [1, 2, 3, 4] -> 1 + 2 + 3 + 4 = 10
È possibile generalizzare tale operazione su qualsiasi tipo di dati ricorsivo.
Quella che segue è una firma della foldrfunzione:
foldr :: ((a -> b -> b), b) -> [a] -> b
Nota che ho usato delle parentesi graffe per separare i primi due argomenti dall'ultimo. Questa non è una vera foldrfunzione, ma è isomorfa (cioè puoi facilmente ottenerne una dall'altra e viceversa). Applicato parzialmente foldravrà la seguente firma:
foldr ((+), 0) :: [Int] -> Int
Possiamo vedere che questa è una funzione che accetta un elenco di numeri interi e restituisce un singolo numero intero. Definiamo tale funzione in termini del nostro IntListtipo.
sumFold :: IntList -> Int
sumFold Nil = 0
sumFold (Cons x xs) = x + sumFold xs
Vediamo che questa funzione è composta da due parti: la prima parte definisce il comportamento di questa funzione su Nilparte IntListe la seconda parte definisce il comportamento della funzione su Consparte.
Supponiamo ora che stiamo programmando non in Haskell ma in qualche linguaggio che consenta l'uso di tipi algebrici direttamente nelle firme dei tipi (beh, tecnicamente Haskell consente l'uso di tipi algebrici tramite tuple e Either a btipo di dati, ma questo porterà a verbosità inutili). Considera una funzione:
reductor :: () | (Int × Int) -> Int
reductor () = 0
reductor (x, s) = x + s
Si può vedere che reductorè una funzione di tipo F1 Int -> Int, proprio come nella definizione di F-algebra! In effetti, la coppia (Int, reductor)è una algebra di F1.
Perché IntListè un'algebra F1 iniziale, per ogni tipo Te per ogni funzione r :: F1 T -> Tesiste una funzione, chiamata catamorfismo per r, che si converte IntListin T, e tale funzione è unica. In effetti, nel nostro esempio un catamorfismo reductorè sumFold. Nota come reductore sumFoldsono simili: hanno quasi la stessa struttura! In reductordefinizione sUtilizzo parametro (tipo del quale corrisponde a T) corrisponde all'uso del risultato di calcolo di sumFold xsin sumFolddefinizione.
Giusto per renderlo più chiaro e aiutarti a vedere lo schema, ecco un altro esempio e ricominciamo dalla risultante funzione di piegatura. Considera la appendfunzione che aggiunge il suo primo argomento al secondo:
(append [4, 5, 6]) [1, 2, 3] = (foldr (:) [4, 5, 6]) [1, 2, 3] -> [1, 2, 3, 4, 5, 6]
Ecco come appare sul nostro IntList:
appendFold :: IntList -> IntList -> IntList
appendFold ys () = ys
appendFold ys (Cons x xs) = x : appendFold ys xs
Ancora una volta, proviamo a scrivere il reduttore:
appendReductor :: IntList -> () | (Int × IntList) -> IntList
appendReductor ys () = ys
appendReductor ys (x, rs) = x : rs
appendFoldè un catamorphism per appendReductorche trasforma IntListin IntList.
Quindi, essenzialmente, le algebre F ci consentono di definire "pieghe" su strutture di dati ricorsive, cioè operazioni che riducono le nostre strutture a un certo valore.
F-coalgebre
Le F-coalgebre sono il cosiddetto termine "doppio" per le F-algebre. Ci consentono di definire unfoldstipi di dati ricorsivi, ovvero un modo per costruire strutture ricorsive da un certo valore.
Supponiamo di avere il seguente tipo:
data IntStream = Cons (Int, IntStream)
Questo è un flusso infinito di numeri interi. Il suo unico costruttore ha il seguente tipo:
Cons :: (Int, IntStream) -> IntStream
O, in termini di set
Cons :: Int × IntStream -> IntStream
Haskell ti consente di modellare la corrispondenza sui costruttori di dati, in modo da poter definire le seguenti funzioni lavorando su IntStreams:
head :: IntStream -> Int
head (Cons (x, xs)) = x
tail :: IntStream -> IntStream
tail (Cons (x, xs)) = xs
Puoi naturalmente "unire" queste funzioni in un'unica funzione di tipo IntStream -> Int × IntStream:
head&tail :: IntStream -> Int × IntStream
head&tail (Cons (x, xs)) = (x, xs)
Notare come il risultato della funzione coincida con la rappresentazione algebrica del nostro IntStreamtipo. Una cosa simile può essere fatta anche per altri tipi di dati ricorsivi. Forse hai già notato lo schema. Mi riferisco a una famiglia di funzioni di tipo
g :: T -> F T
dov'è un Tcerto tipo. D'ora in poi definiremo
F1 T = Int × T
Ora, F-coalgebra è una coppia (T, g), dove Tè un tipo ed gè una funzione del tipo g :: T -> F T. Ad esempio, (IntStream, head&tail)è una coalgebra F1. Ancora una volta, proprio come nelle algebre F, ge Tpuò essere arbitrario, per esempio, (String, h :: String -> Int x String)è anche una coalgebra F1 per qualche ora.
Tra tutte le coalgebre F ci sono le cosiddette coalgebre F terminali , che sono doppie rispetto alle algebre F iniziali. Ad esempio, IntStreamè una F-coalgebra terminale. Ciò significa che per ogni tipo Te per ogni funzione p :: T -> F1 Tesiste una funzione, chiamata anamorfismo , che si converte Tin IntStream, e tale funzione è unica.
Considera la seguente funzione, che genera un flusso di numeri interi successivi a partire da quello indicato:
nats :: Int -> IntStream
nats n = Cons (n, nats (n+1))
Ora esaminiamo una funzione natsBuilder :: Int -> F1 Int, ovvero natsBuilder :: Int -> Int × Int:
natsBuilder :: Int -> Int × Int
natsBuilder n = (n, n+1)
Ancora una volta, possiamo vedere alcune somiglianze tra natse natsBuilder. È molto simile alla connessione che abbiamo osservato in precedenza con riduttori e pieghe. natsè un anamorfismo per natsBuilder.
Un altro esempio, una funzione che accetta un valore e una funzione e restituisce al flusso un flusso di applicazioni successive della funzione:
iterate :: (Int -> Int) -> Int -> IntStream
iterate f n = Cons (n, iterate f (f n))
La sua funzione builder è la seguente:
iterateBuilder :: (Int -> Int) -> Int -> Int × Int
iterateBuilder f n = (n, f n)
Quindi iterateè un anamorfismo per iterateBuilder.
Conclusione
Quindi, in breve, le algebre F consentono di definire le pieghe, ovvero le operazioni che riducono la struttura ricorsiva in un singolo valore, e le coalgebre F consentono di fare il contrario: costruire una struttura [potenzialmente] infinita da un singolo valore.
In Haskell F-algebre e F-coalgebre coincidono infatti. Questa è una proprietà molto bella che è una conseguenza della presenza del valore 'bottom' in ogni tipo. Quindi in Haskell è possibile creare sia le pieghe che le spiegazioni per ogni tipo ricorsivo. Tuttavia, il modello teorico dietro questo è più complesso di quello che ho presentato sopra, quindi l'ho deliberatamente evitato.
Spero che questo ti aiuti.