F-álgebras y F-coalgebras son estructuras matemáticas que son instrumentales en el razonamiento sobre los tipos inductivos (o tipos recursivos ).
Álgebras F
Comenzaremos primero con F-álgebras. Intentaré ser lo más simple posible.
Supongo que sabes lo que es un tipo recursivo. Por ejemplo, este es un tipo para una lista de enteros:
data IntList = Nil | Cons (Int, IntList)
Es obvio que es recursivo; de hecho, su definición se refiere a sí misma. Su definición consta de dos constructores de datos, que tienen los siguientes tipos:
Nil :: () -> IntList
Cons :: (Int, IntList) -> IntList
Tenga en cuenta que he escrito tipo de Nil
como () -> IntList
, no simplemente IntList
. De hecho, estos son tipos equivalentes desde el punto de vista teórico, porque el ()
tipo tiene solo un habitante.
Si escribimos firmas de estas funciones de una manera más teórica, obtendremos
Nil :: 1 -> IntList
Cons :: Int × IntList -> IntList
donde 1
es un conjunto de unidades (conjunto con un elemento) y la A × B
operación es un producto cruzado de dos conjuntos A
y B
(es decir, un conjunto de pares (a, b)
donde a
atraviesa todos los elementos A
y b
atraviesa todos los elementos de B
).
Unión disjunta de dos conjuntos A
y B
es un conjunto A | B
que es una unión de conjuntos {(a, 1) : a in A}
y {(b, 2) : b in B}
. En esencia se trata de un conjunto de todos los elementos de ambos A
y B
, pero con cada uno de estos elementos 'marcado' como perteneciente a cualquiera de los dos A
o B
, por lo que cuando tomamos cualquier elemento de A | B
sabremos de inmediato si este elemento proviene de A
o desde B
.
Podemos 'unirnos' Nil
y Cons
funciones, por lo que formarán una sola función trabajando en un conjunto 1 | (Int × IntList)
:
Nil|Cons :: 1 | (Int × IntList) -> IntList
De hecho, si la Nil|Cons
función se aplica al ()
valor (que, obviamente, pertenece al 1 | (Int × IntList)
conjunto), entonces se comporta como si lo fuera Nil
; si Nil|Cons
se aplica a cualquier valor de tipo (Int, IntList)
(tales valores también están en el conjunto 1 | (Int × IntList)
, se comporta como Cons
.
Ahora considere otro tipo de datos:
data IntTree = Leaf Int | Branch (IntTree, IntTree)
Tiene los siguientes constructores:
Leaf :: Int -> IntTree
Branch :: (IntTree, IntTree) -> IntTree
que también se puede unir en una función:
Leaf|Branch :: Int | (IntTree × IntTree) -> IntTree
Se puede ver que ambas joined
funciones tienen un tipo similar: ambas parecen
f :: F T -> T
donde F
es una especie de transformación que tiene nuestro tipo y da tipo más complejo, que consiste en x
y |
operaciones, usos de T
y posiblemente otros tipos. Por ejemplo, para IntList
y IntTree
F
tiene el siguiente aspecto:
F1 T = 1 | (Int × T)
F2 T = Int | (T × T)
Podemos notar de inmediato que cualquier tipo algebraico se puede escribir de esta manera. De hecho, es por eso que se les llama 'algebraicos': consisten en una cantidad de 'sumas' (uniones) y 'productos' (productos cruzados) de otros tipos.
Ahora podemos definir F-álgebra. F-álgebra es solo un par (T, f)
, donde T
hay algún tipo y f
es una función de tipo f :: F T -> T
. En nuestros ejemplos, las álgebras F son (IntList, Nil|Cons)
y (IntTree, Leaf|Branch)
. Tenga en cuenta, sin embargo, que a pesar de ese tipo de f
función es la misma para cada F, T
y en f
sí mismas pueden ser arbitrarias. Por ejemplo, (String, g :: 1 | (Int x String) -> String)
o (Double, h :: Int | (Double, Double) -> Double)
para algunos g
y h
también son F-álgebras para F. correspondiente
Luego podemos introducir homomorfismos de álgebra F y luego álgebras F iniciales , que tienen propiedades muy útiles. De hecho, (IntList, Nil|Cons)
es un álgebra F1 inicial, y (IntTree, Leaf|Branch)
es un álgebra F2 inicial. No presentaré definiciones exactas de estos términos y propiedades, ya que son más complejos y abstractos de lo necesario.
No obstante, el hecho de que, digamos, (IntList, Nil|Cons)
es F-álgebra nos permite definir una fold
función similar a este tipo. Como sabe, fold es un tipo de operación que transforma algunos tipos de datos recursivos en un valor finito. Por ejemplo, podemos plegar una lista de enteros en un solo valor, que es la suma de todos los elementos de la lista:
foldr (+) 0 [1, 2, 3, 4] -> 1 + 2 + 3 + 4 = 10
Es posible generalizar dicha operación en cualquier tipo de datos recursivo.
La siguiente es una firma de foldr
función:
foldr :: ((a -> b -> b), b) -> [a] -> b
Tenga en cuenta que he usado llaves para separar los dos primeros argumentos del último. Esta no es una foldr
función real , pero es isomórfica (es decir, puede obtener fácilmente una de la otra y viceversa). Aplicado parcialmente foldr
tendrá la siguiente firma:
foldr ((+), 0) :: [Int] -> Int
Podemos ver que esta es una función que toma una lista de enteros y devuelve un solo entero. Definamos dicha función en términos de nuestro IntList
tipo.
sumFold :: IntList -> Int
sumFold Nil = 0
sumFold (Cons x xs) = x + sumFold xs
Vemos que esta función consta de dos partes: la primera parte define el comportamiento de esta función por Nil
parte de IntList
, y la segunda parte define el comportamiento de la función por Cons
parte.
Ahora suponga que no estamos programando en Haskell sino en algún lenguaje que permita el uso de tipos algebraicos directamente en firmas de tipo (bueno, técnicamente Haskell permite el uso de tipos algebraicos a través de tuplas y Either a b
tipos de datos, pero esto conducirá a verbosidades innecesarias). Considere una función:
reductor :: () | (Int × Int) -> Int
reductor () = 0
reductor (x, s) = x + s
Se puede ver que reductor
es una función de tipo F1 Int -> Int
, ¡como en la definición de F-álgebra! De hecho, el par (Int, reductor)
es un álgebra de F1.
Debido a que IntList
es una F1-álgebra inicial, para cada tipo T
y para cada función r :: F1 T -> T
no existe una función, llamada catamorphism para r
, que convierte IntList
a T
, y tal función es única. De hecho, en nuestro ejemplo un catamorphism para reductor
es sumFold
. Tenga en cuenta cómo reductor
y sumFold
son similares: ¡tienen casi la misma estructura! El uso del parámetro en reductor
definición s
(cuyo tipo corresponde a T
) corresponde al uso del resultado del cálculo de sumFold xs
en sumFold
definición.
Solo para hacerlo más claro y ayudarlo a ver el patrón, aquí hay otro ejemplo, y nuevamente comenzamos desde la función de plegado resultante. Considere la append
función que agrega su primer argumento al segundo argumento:
(append [4, 5, 6]) [1, 2, 3] = (foldr (:) [4, 5, 6]) [1, 2, 3] -> [1, 2, 3, 4, 5, 6]
Así se ve en nuestro IntList
:
appendFold :: IntList -> IntList -> IntList
appendFold ys () = ys
appendFold ys (Cons x xs) = x : appendFold ys xs
Nuevamente, intentemos escribir el reductor:
appendReductor :: IntList -> () | (Int × IntList) -> IntList
appendReductor ys () = ys
appendReductor ys (x, rs) = x : rs
appendFold
Es un catamorfismo para el appendReductor
que se transforma IntList
en IntList
.
Entonces, esencialmente, las álgebras F nos permiten definir 'pliegues' en estructuras de datos recursivas, es decir, operaciones que reducen nuestras estructuras a algún valor.
F-coalgebras
F-coalgebras se denominan término 'dual' para F-álgebras. Nos permiten definir unfolds
tipos de datos recursivos, es decir, una forma de construir estructuras recursivas a partir de algún valor.
Supongamos que tiene el siguiente tipo:
data IntStream = Cons (Int, IntStream)
Este es un flujo infinito de enteros. Su único constructor tiene el siguiente tipo:
Cons :: (Int, IntStream) -> IntStream
O, en términos de conjuntos
Cons :: Int × IntStream -> IntStream
Haskell le permite emparejar patrones en constructores de datos, para que pueda definir las siguientes funciones trabajando en IntStream
s:
head :: IntStream -> Int
head (Cons (x, xs)) = x
tail :: IntStream -> IntStream
tail (Cons (x, xs)) = xs
Naturalmente, puede 'unir' estas funciones en una sola función de tipo IntStream -> Int × IntStream
:
head&tail :: IntStream -> Int × IntStream
head&tail (Cons (x, xs)) = (x, xs)
Observe cómo el resultado de la función coincide con la representación algebraica de nuestro IntStream
tipo. Algo similar también se puede hacer para otros tipos de datos recursivos. Quizás ya hayas notado el patrón. Me refiero a una familia de funciones de tipo
g :: T -> F T
donde T
hay algun tipo De ahora en adelante definiremos
F1 T = Int × T
Ahora, F-coalgebra es un par (T, g)
, donde T
es un tipo y g
es una función del tipo g :: T -> F T
. Por ejemplo, (IntStream, head&tail)
es un F1-coalgebra. De nuevo, al igual que en F-álgebras, g
y T
puede ser arbitrario, por ejemplo, (String, h :: String -> Int x String)
también es un F1-coalgebra por alguna h.
Entre todas las F-coalgebras hay las llamadas F-coalgebras terminales , que son duales a las F-álgebras iniciales. Por ejemplo, IntStream
es un terminal F-coalgebra. Esto significa que para cada tipo T
y para cada función p :: T -> F1 T
existe una función, llamada anamorfosis , que convierte T
a IntStream
, y tal función es única.
Considere la siguiente función, que genera un flujo de enteros sucesivos a partir del dado:
nats :: Int -> IntStream
nats n = Cons (n, nats (n+1))
Ahora inspeccionemos una función natsBuilder :: Int -> F1 Int
, es decir natsBuilder :: Int -> Int × Int
:
natsBuilder :: Int -> Int × Int
natsBuilder n = (n, n+1)
De nuevo, podemos ver cierta similitud entre nats
y natsBuilder
. Es muy similar a la conexión que hemos observado con reductores y pliegues anteriormente. nats
Es un anamorfismo para natsBuilder
.
Otro ejemplo, una función que toma un valor y una función y devuelve un flujo de aplicaciones sucesivas de la función al valor:
iterate :: (Int -> Int) -> Int -> IntStream
iterate f n = Cons (n, iterate f (f n))
Su función constructora es la siguiente:
iterateBuilder :: (Int -> Int) -> Int -> Int × Int
iterateBuilder f n = (n, f n)
Entonces iterate
es un anamorfismo para iterateBuilder
.
Conclusión
En resumen, las álgebras F permiten definir pliegues, es decir, operaciones que reducen la estructura recursiva a un valor único, y las álgebras F permiten hacer lo contrario: construir una estructura [potencialmente] infinita a partir de un valor único.
De hecho, en Haskell F-álgebras y F-coalgebras coinciden. Esta es una propiedad muy agradable que es consecuencia de la presencia del valor 'inferior' en cada tipo. Por lo tanto, en Haskell se pueden crear pliegues y despliegues para cada tipo recursivo. Sin embargo, el modelo teórico detrás de esto es más complejo que el que he presentado anteriormente, por lo que deliberadamente lo he evitado.
Espero que esto ayude.