Álgebras-F e barras-carvão são estruturas matemáticas que são instrumentais no raciocínio sobre tipos indutivos (ou recursivos ).
Álgebras F
Vamos começar primeiro com álgebras F. Vou tentar ser o mais simples possível.
Eu acho que você sabe o que é um tipo recursivo. Por exemplo, este é um tipo para uma lista de números inteiros:
data IntList = Nil | Cons (Int, IntList)
É óbvio que é recursivo - de fato, sua definição se refere a si mesma. Sua definição consiste em dois construtores de dados, que têm os seguintes tipos:
Nil :: () -> IntList
Cons :: (Int, IntList) -> IntList
Note que eu escrevi o tipo de Nil
as () -> IntList
, não simplesmente IntList
. De fato, são tipos equivalentes do ponto de vista teórico, porque o ()
tipo tem apenas um habitante.
Se escrevermos assinaturas dessas funções de uma maneira mais teórica, obteremos
Nil :: 1 -> IntList
Cons :: Int × IntList -> IntList
onde 1
é um conjunto de unidades (conjunto com um elemento) e A × B
operação é um produto cruzado de dois conjuntos A
e B
(ou seja, conjunto de pares (a, b)
onde a
passa por todos os elementos de A
e b
passa por todos os elementos de B
).
União disjunta de dois conjuntos A
e B
é um conjunto A | B
que é uma união de conjuntos {(a, 1) : a in A}
e {(b, 2) : b in B}
. Essencialmente, é um conjunto de todos os elementos de ambos A
e B
, mas com cada um desses elementos 'marcados' como pertencentes a um A
ou outro B
, portanto, quando escolhermos qualquer elemento, A | B
saberemos imediatamente se esse elemento veio A
ou não B
.
Podemos 'juntar-se' Nil
e Cons
funções, para que eles formem uma única função trabalhando em um conjunto 1 | (Int × IntList)
:
Nil|Cons :: 1 | (Int × IntList) -> IntList
De fato, se a Nil|Cons
função é aplicada ao ()
valor (que obviamente pertence ao 1 | (Int × IntList)
conjunto), ela se comporta como se fosse Nil
; se Nil|Cons
for aplicado a qualquer valor do tipo (Int, IntList)
(esses valores também estão no conjunto 1 | (Int × IntList)
, ele se comporta como Cons
.
Agora considere outro tipo de dados:
data IntTree = Leaf Int | Branch (IntTree, IntTree)
Possui os seguintes construtores:
Leaf :: Int -> IntTree
Branch :: (IntTree, IntTree) -> IntTree
que também pode ser associado a uma função:
Leaf|Branch :: Int | (IntTree × IntTree) -> IntTree
Pode-se ver que essas duas joined
funções têm um tipo semelhante: ambas se parecem
f :: F T -> T
onde F
é um tipo de transformação que leva o nosso tipo e dá tipo mais complexo, que consiste em x
e |
operações, usos T
e possivelmente outros tipos. Por exemplo, para IntList
e IntTree
F
tem a seguinte aparência:
F1 T = 1 | (Int × T)
F2 T = Int | (T × T)
Podemos notar imediatamente que qualquer tipo algébrico pode ser escrito dessa maneira. De fato, é por isso que eles são chamados de 'algébricos': eles consistem em um número de 'somas' (uniões) e 'produtos' (produtos cruzados) de outros tipos.
Agora podemos definir álgebra F. A álgebra F é apenas um par (T, f)
, onde T
é algum tipo e f
é uma função do tipo f :: F T -> T
. Nos nossos exemplos, as álgebras F são (IntList, Nil|Cons)
e (IntTree, Leaf|Branch)
. Observe, no entanto, que, apesar desse tipo de f
função, é o mesmo para cada F, T
e f
elas próprias podem ser arbitrárias. Por exemplo, (String, g :: 1 | (Int x String) -> String)
ou (Double, h :: Int | (Double, Double) -> Double)
para alguns, g
e h
também são álgebras F para F. correspondente
Posteriormente, podemos introduzir homomorfismos da álgebra F e, em seguida , álgebras F iniciais , que possuem propriedades muito úteis. De fato, (IntList, Nil|Cons)
é uma álgebra F1 inicial e (IntTree, Leaf|Branch)
é uma álgebra F2 inicial. Não apresentarei definições exatas desses termos e propriedades, pois são mais complexos e abstratos do que o necessário.
No entanto, o fato de, digamos, (IntList, Nil|Cons)
ser álgebra F, permite definir fold
funções semelhantes a esse tipo. Como você sabe, fold é um tipo de operação que transforma algum tipo de dados recursivo em um valor finito. Por exemplo, podemos dobrar uma lista de números inteiros em um único valor, que é uma soma de todos os elementos da lista:
foldr (+) 0 [1, 2, 3, 4] -> 1 + 2 + 3 + 4 = 10
É possível generalizar essa operação em qualquer tipo de dados recursivo.
A seguir, é uma assinatura da foldr
função:
foldr :: ((a -> b -> b), b) -> [a] -> b
Observe que usei chaves para separar os dois primeiros argumentos do último. Essa não é uma foldr
função real , mas é isomórfica (ou seja, você pode facilmente obter uma da outra e vice-versa). Parcialmente aplicado foldr
terá a seguinte assinatura:
foldr ((+), 0) :: [Int] -> Int
Podemos ver que essa é uma função que pega uma lista de números inteiros e retorna um único número inteiro. Vamos definir essa função em termos do nosso IntList
tipo.
sumFold :: IntList -> Int
sumFold Nil = 0
sumFold (Cons x xs) = x + sumFold xs
Vemos que essa função consiste em duas partes: a primeira parte define o comportamento dessa função em Nil
parte IntList
e a segunda parte define o comportamento da função em Cons
parte.
Agora, suponha que não estamos programando em Haskell, mas em alguma linguagem que permita o uso de tipos algébricos diretamente nas assinaturas de tipo (bem, tecnicamente, o Haskell permite o uso de tipos algébricos por tuplas e Either a b
tipos de dados, mas isso levará a verbosidade desnecessária). Considere uma função:
reductor :: () | (Int × Int) -> Int
reductor () = 0
reductor (x, s) = x + s
Pode-se ver que reductor
é uma função do tipo F1 Int -> Int
, assim como na definição da álgebra F! De fato, o par (Int, reductor)
é uma álgebra F1.
Como IntList
é uma álgebra F1 inicial, para cada tipo T
e função r :: F1 T -> T
existe uma função chamada catamorfismo para r
, que se converte IntList
em T
, e essa função é única. De fato, em nosso exemplo um catamorphism para reductor
é sumFold
. Observe como reductor
e sumFold
são semelhantes: eles têm quase a mesma estrutura! Na reductor
definição, o s
uso do parâmetro (do tipo corresponde a T
) corresponde ao uso do resultado da computação de sumFold xs
na sumFold
definição.
Apenas para torná-lo mais claro e ajudá-lo a ver o padrão, aqui está outro exemplo, e começamos novamente a partir da função de dobra resultante. Considere a append
função que anexa seu primeiro argumento ao segundo:
(append [4, 5, 6]) [1, 2, 3] = (foldr (:) [4, 5, 6]) [1, 2, 3] -> [1, 2, 3, 4, 5, 6]
É assim que fica no nosso IntList
:
appendFold :: IntList -> IntList -> IntList
appendFold ys () = ys
appendFold ys (Cons x xs) = x : appendFold ys xs
Novamente, vamos tentar escrever o redutor:
appendReductor :: IntList -> () | (Int × IntList) -> IntList
appendReductor ys () = ys
appendReductor ys (x, rs) = x : rs
appendFold
é um catamorphism para appendReductor
que transforma IntList
em IntList
.
Portanto, essencialmente, as álgebras F permitem definir 'dobras' em estruturas de dados recursivas, ou seja, operações que reduzem nossas estruturas a algum valor.
F-coalgebras
F-coalgebras são os chamados termos 'duplos' para álgebras F. Eles nos permitem definir unfolds
tipos de dados recursivos, ou seja, uma maneira de construir estruturas recursivas a partir de algum valor.
Suponha que você tenha o seguinte tipo:
data IntStream = Cons (Int, IntStream)
Este é um fluxo infinito de números inteiros. Seu único construtor tem o seguinte tipo:
Cons :: (Int, IntStream) -> IntStream
Ou, em termos de conjuntos
Cons :: Int × IntStream -> IntStream
O Haskell permite padronizar a correspondência nos construtores de dados, para que você possa definir as seguintes funções trabalhando em IntStream
s:
head :: IntStream -> Int
head (Cons (x, xs)) = x
tail :: IntStream -> IntStream
tail (Cons (x, xs)) = xs
Naturalmente, você pode 'unir' essas funções em uma única função do tipo IntStream -> Int × IntStream
:
head&tail :: IntStream -> Int × IntStream
head&tail (Cons (x, xs)) = (x, xs)
Observe como o resultado da função coincide com a representação algébrica do nosso IntStream
tipo. O mesmo pode ser feito para outros tipos de dados recursivos. Talvez você já tenha notado o padrão. Estou me referindo a uma família de funções do tipo
g :: T -> F T
Onde T
está algum tipo. A partir de agora vamos definir
F1 T = Int × T
Agora, F-coalgebra é um par (T, g)
, onde T
é um tipo e g
é uma função do tipo g :: T -> F T
. Por exemplo, (IntStream, head&tail)
é um F1-coalgebra. Novamente, assim como nas álgebras F, g
e T
pode ser arbitrário, por exemplo, (String, h :: String -> Int x String)
também é uma álgebra F1 por alguns h.
Entre todas as barras de carvão F, existem as chamadas barras de terminal F , que são duplas às álgebras F iniciais. Por exemplo, IntStream
é um terminal F-coalgebra. Isso significa que, para todo tipo T
e função p :: T -> F1 T
, existe uma função chamada anamorfismo , que se converte T
em IntStream
, e essa função é única.
Considere a seguinte função, que gera um fluxo de números inteiros sucessivos a partir do dado:
nats :: Int -> IntStream
nats n = Cons (n, nats (n+1))
Agora vamos inspecionar uma função natsBuilder :: Int -> F1 Int
, ou seja natsBuilder :: Int -> Int × Int
:
natsBuilder :: Int -> Int × Int
natsBuilder n = (n, n+1)
Novamente, podemos ver alguma semelhança entre nats
e natsBuilder
. É muito semelhante à conexão que observamos anteriormente com redutores e dobras. nats
é um anamorfismo para natsBuilder
.
Outro exemplo, uma função que pega um valor e uma função e retorna um fluxo de aplicativos sucessivos da função para o valor:
iterate :: (Int -> Int) -> Int -> IntStream
iterate f n = Cons (n, iterate f (f n))
Sua função de construtor é a seguinte:
iterateBuilder :: (Int -> Int) -> Int -> Int × Int
iterateBuilder f n = (n, f n)
Então iterate
é um anamorfismo para iterateBuilder
.
Conclusão
Então, em resumo, as álgebras-F permitem definir dobras, isto é, operações que reduzem a estrutura recursiva em um único valor, e as álgebras-F permitem fazer o oposto: construir uma estrutura [potencialmente infinita] a partir de um único valor.
De fato, as álgebras F e as coalgebras de Haskell coincidem. Esta é uma propriedade muito agradável, que é uma conseqüência da presença do valor 'bottom' em cada tipo. Assim, em Haskell, podem ser criadas dobras e desdobramentos para todos os tipos recursivos. No entanto, o modelo teórico por trás disso é mais complexo do que o que apresentei acima, por isso evitei-o deliberadamente.
Espero que isto ajude.