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 Nil
as () -> 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 × B
operazione è un prodotto incrociato di due insiemi A
e B
(ovvero, un insieme di coppie in (a, b)
cui a
attraversa tutti gli elementi A
e b
attraversa tutti gli elementi di B
).
Unione disgiunta di due insiemi A
ed B
è un insieme A | B
che è un'unione di insiemi {(a, 1) : a in A}
e {(b, 2) : b in B}
. Essenzialmente è un insieme di tutti gli elementi di entrambi A
e B
, ma con ciascuno di questi elementi 'marcati' come appartenenti a uno A
o B
, quindi quando scegliamo qualsiasi elemento da A | B
sapremo immediatamente se questo elemento proviene da A
o da B
.
Possiamo "unire" Nil
e Cons
funzioni, quindi formeranno un'unica funzione che lavora su un set 1 | (Int × IntList)
:
Nil|Cons :: 1 | (Int × IntList) -> IntList
Infatti, se la Nil|Cons
funzione viene applicata al ()
valore (che, ovviamente, appartiene al 1 | (Int × IntList)
set), allora si comporta come se fosse Nil
; se Nil|Cons
viene 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 joined
funzioni hanno un tipo simile: sembrano entrambe
f :: F T -> T
dove F
c'è un tipo di trasformazione che prende il nostro tipo e dà un tipo più complesso, che consiste di x
e |
operazioni, usi T
e possibilmente altri tipi. Ad esempio, per IntList
e si IntTree
F
presenta 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 T
esiste 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 f
funzione è lo stesso per ogni F, T
e f
possono essere arbitrari. Ad esempio, (String, g :: 1 | (Int x String) -> String)
o (Double, h :: Int | (Double, Double) -> Double)
per alcuni g
e h
sono 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 fold
funzione 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 foldr
funzione:
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 foldr
funzione, ma è isomorfa (cioè puoi facilmente ottenerne una dall'altra e viceversa). Applicato parzialmente foldr
avrà 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 IntList
tipo.
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 Nil
parte IntList
e la seconda parte definisce il comportamento della funzione su Cons
parte.
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 b
tipo 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 T
e per ogni funzione r :: F1 T -> T
esiste una funzione, chiamata catamorfismo per r
, che si converte IntList
in T
, e tale funzione è unica. In effetti, nel nostro esempio un catamorfismo reductor
è sumFold
. Nota come reductor
e sumFold
sono simili: hanno quasi la stessa struttura! In reductor
definizione s
Utilizzo parametro (tipo del quale corrisponde a T
) corrisponde all'uso del risultato di calcolo di sumFold xs
in sumFold
definizione.
Giusto per renderlo più chiaro e aiutarti a vedere lo schema, ecco un altro esempio e ricominciamo dalla risultante funzione di piegatura. Considera la append
funzione 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 appendReductor
che trasforma IntList
in 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 unfolds
tipi 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 IntStream
s:
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 IntStream
tipo. 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 T
certo 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, g
e T
può 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 T
e per ogni funzione p :: T -> F1 T
esiste una funzione, chiamata anamorfismo , che si converte T
in 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 nats
e 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.