monoid functors fmap haskell functor generic-programming

haskell - functors - Indexación en contenedores: los fundamentos matemáticos.



fmap haskell (1)

Parece que el índice en el tipo es un índice en el conjunto de constructores, seguido por un índice en el producto que representa ese constructor. Esto se puede implementar de forma bastante natural, por ejemplo, con generics-sop .

Primero necesita un tipo de datos para representar índices posibles en un solo elemento del producto. Esto podría ser un índice que apunta a un elemento de tipo a , o un índice que apunta a algo de tipo gb , que requiere un índice que apunta a g un índice que apunta a un elemento de tipo a en b . Esto está codificado con el siguiente tipo:

import Generics.SOP data ArgIx f x x'' where Here :: ArgIx f x x There :: (Generic (g x'')) => Ix g -> ArgIx f x x'' -> ArgIx f x (g x'') newtype Ix f = ...

El índice en sí es solo una suma (implementada por NS para la suma n-aria) de sumas sobre la representación genérica del tipo (elección del constructor, elección del elemento constructor):

newtype Ix f = MkIx (forall x . NS (NS (ArgIx f x)) (Code (f x)))

Puedes escribir constructores inteligentes para varios índices:

listIx :: Natural -> Ix [] listIx 0 = MkIx $ S $ Z $ Z Here listIx k = MkIx $ S $ Z $ S $ Z $ There (listIx (k-1)) Here treeIx :: [Bool] -> Ix Tree treeIx [] = MkIx $ S $ Z $ S $ Z Here treeIx (b:bs) = case b of True -> MkIx $ S $ Z $ Z $ There (treeIx bs) Here False -> MkIx $ S $ Z $ S $ S $ Z $ There (treeIx bs) Here roseIx :: [Natural] -> Ix Rose roseIx [] = MkIx $ Z $ Z Here roseIx (k:ks) = MkIx $ Z $ S $ Z $ There (listIx k) (There (roseIx ks) Here)

Tenga en cuenta que, por ejemplo, en el caso de la lista, no puede construir un índice (no inferior) que apunte al constructor [] , del mismo modo para Tree y Empty , o constructores que contengan valores cuyo tipo no sea a o algo que contenga algunos valores de tipo a . La cuantificación en MkIx evita que las cosas malas de la construcción, como un índice que apunta al primer Int en los data X x = X Int x donde x se ejemplifica a Int .

La implementación de la función de índice es bastante sencilla, incluso si los tipos dan miedo:

(!) :: (Generic (f x)) => f x -> Ix f -> Maybe x (!) arg (MkIx ix) = go (unSOP $ from arg) ix where atIx :: a -> ArgIx f x a -> Maybe x atIx a Here = Just a atIx a (There ix0 ix1) = a ! ix0 >>= flip atIx ix1 go :: (All SListI xss) => NS (NP I) xss -> NS (NS (ArgIx f x)) xss -> Maybe x go (Z a) (Z b) = hcollapse $ hzipWith (/(I x) -> K . atIx x) a b go (S x) (S x'') = go x x'' go Z{} S{} = Nothing go S{} Z{} = Nothing

La función go compara el constructor al que apunta el índice y el constructor real utilizado por el tipo. Si los constructores no coinciden, la indexación devuelve Nothing . Si lo hacen, se realiza la indexación real, lo cual es trivial en el caso de que el índice apunte exactamente Here , y en el caso de alguna subestructura, ambas operaciones de indexación deben suceder una después de la otra, que es manejada por >>= .

Y una prueba sencilla:

>map (("hello" !) . listIx) [0..5] [Just ''h'',Just ''e'',Just ''l'',Just ''l'',Just ''o'',Nothing]

Cuando quiere sacar un elemento de una estructura de datos, tiene que dar su índice. Pero el significado del índice depende de la propia estructura de datos.

class Indexed f where type Ix f (!) :: f a -> Ix f -> Maybe a -- indices can be out of bounds

Por ejemplo...

Los elementos en una lista tienen posiciones numéricas.

data Nat = Z | S Nat instance Indexed [] where type Ix [] = Nat [] ! _ = Nothing (x:_) ! Z = Just x (_:xs) ! (S n) = xs ! n

Los elementos en un árbol binario se identifican por una secuencia de direcciones.

data Tree a = Leaf | Node (Tree a) a (Tree a) data TreeIx = Stop | GoL TreeIx | GoR TreeIx -- equivalently [Bool] instance Indexed Tree where type Ix Tree = TreeIx Leaf ! _ = Nothing Node l x r ! Stop = Just x Node l x r ! GoL i = l ! i Node l x r ! GoR j = r ! j

Buscar algo en un rosal implica bajar los niveles uno por uno seleccionando un árbol del bosque en cada nivel.

data Rose a = Rose a [Rose a] -- I don''t even like rosé data RoseIx = Top | Down Nat RoseIx -- equivalently [Nat] instance Indexed Rose where type Ix Rose = RoseIx Rose x ts ! Top = Just x Rose x ts ! Down i j = ts ! i >>= (! j)

Parece que el índice de un tipo de producto es una suma (que le indica qué brazo del producto debe mirar), que el índice de un elemento es el tipo de unidad y que el índice de un tipo anidado es un producto (que le indica dónde debe buscar en el tipo anidado). Las sumas parecen ser las únicas que no están vinculadas de alguna manera al derivative . El índice de una suma también es una suma: te dice qué parte de la suma que el usuario espera encontrar, y si se viola esa expectativa, te quedas con un puñado de Nothing .

De hecho, tuve algo de éxito implementando ! genéricamente para funtores definidos como el punto fijo de un bifunctor polinomial. No entraré en detalles, pero Fix f se puede convertir en una instancia de Indexed cuando f es una instancia de Indexed2 ...

class Indexed2 f where type IxA f type IxB f ixA :: f a b -> IxA f -> Maybe a ixB :: f a b -> IxB f -> Maybe b

... y resulta que puede definir una instancia de Indexed2 para cada uno de los bloques de construcción bifunctor.

Pero, ¿qué está pasando realmente? ¿Cuál es la relación subyacente entre un funtor y su índice? ¿Cómo se relaciona con la derivada del funtor? ¿Es necesario comprender la teoría de los contenedores (que yo no, realmente) para responder esta pregunta?