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 Nilcomo () -> 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 1es un conjunto de unidades (conjunto con un elemento) y la A × Boperación es un producto cruzado de dos conjuntos Ay B(es decir, un conjunto de pares (a, b)donde aatraviesa todos los elementos Ay batraviesa todos los elementos de B).
Unión disjunta de dos conjuntos Ay Bes un conjunto A | Bque 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 Ay B, pero con cada uno de estos elementos 'marcado' como perteneciente a cualquiera de los dos Ao B, por lo que cuando tomamos cualquier elemento de A | Bsabremos de inmediato si este elemento proviene de Ao desde B.
Podemos 'unirnos' Nily Consfunciones, 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|Consfunción se aplica al ()valor (que, obviamente, pertenece al 1 | (Int × IntList)conjunto), entonces se comporta como si lo fuera Nil; si Nil|Consse 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 joinedfunciones tienen un tipo similar: ambas parecen
f :: F T -> T
donde Fes una especie de transformación que tiene nuestro tipo y da tipo más complejo, que consiste en xy |operaciones, usos de Ty posiblemente otros tipos. Por ejemplo, para IntListy IntTree Ftiene 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 Thay algún tipo y fes 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 ffunción es la misma para cada F, Ty en fsí mismas pueden ser arbitrarias. Por ejemplo, (String, g :: 1 | (Int x String) -> String)o (Double, h :: Int | (Double, Double) -> Double)para algunos gy htambié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 foldfunció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 foldrfunció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 foldrfunción real , pero es isomórfica (es decir, puede obtener fácilmente una de la otra y viceversa). Aplicado parcialmente foldrtendrá 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 IntListtipo.
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 Nilparte de IntList, y la segunda parte define el comportamiento de la función por Consparte.
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 btipos 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 reductores 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 IntListes una F1-álgebra inicial, para cada tipo Ty para cada función r :: F1 T -> Tno existe una función, llamada catamorphism para r, que convierte IntLista T, y tal función es única. De hecho, en nuestro ejemplo un catamorphism para reductores sumFold. Tenga en cuenta cómo reductory sumFoldson similares: ¡tienen casi la misma estructura! El uso del parámetro en reductordefinición s(cuyo tipo corresponde a T) corresponde al uso del resultado del cálculo de sumFold xsen sumFolddefinició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 appendfunció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
appendFoldEs un catamorfismo para el appendReductorque se transforma IntListen 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 unfoldstipos 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 IntStreams:
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 IntStreamtipo. 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 Thay algun tipo De ahora en adelante definiremos
F1 T = Int × T
Ahora, F-coalgebra es un par (T, g), donde Tes un tipo y ges 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, gy Tpuede 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, IntStreames un terminal F-coalgebra. Esto significa que para cada tipo Ty para cada función p :: T -> F1 Texiste una función, llamada anamorfosis , que convierte Ta 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 natsy natsBuilder. Es muy similar a la conexión que hemos observado con reductores y pliegues anteriormente. natsEs 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 iteratees 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.