haskell monoids

Monoides y Num en Haskell



monoids (3)

Existe (implícitamente, si no explícitamente), una instancia de monoide para funciones ordinarias de la forma a -> a , donde mappend corresponde a la composición de la función, y mempty corresponde a la función id .

¿Qué es (+) ? Es una función (Num a) => a -> a -> a . Si foldMap el foldMap sobre tu número de números plegable con + , conviertes cada uno en el parcialmente aplicado (+ <some number) , que es un a -> a . ¡Y he aquí que has encontrado la magia que convertirá todo en tu plegable en un monoide!

Suponiendo que hubiera una instancia de monoide directa para funciones, podría hacer:

foldMap (+) [1, 2, 3, 4]

, que produciría un final (Num a) => a -> a que podría aplicar a 0 para obtener 10 .

Sin embargo, no existe tal instancia directa, por lo que necesita usar el envoltorio de newtype incorporado Endo y la aplicación correspondiente de appEndo , que implementa appEndo para a -> a funciones. Esto es lo que parece:

Prelude Data.Monoid> (appEndo (foldMap (Endo . (+)) [1, 2, 3, 4])) 0 10

Aquí Endo . es solo nuestra molesta necesidad de levantar la llanura a -> a s para que tengan su instancia natural de Monoid . Después de que foldMap reducir nuestro plegado convirtiendo todo en a -> a sy encadenándolos con la composición, extraemos el a -> a final usando appEndo y finalmente lo aplicamos a 0 .

He estado aprendiendo Haskell durante los últimos meses y me encontré con un ejemplo de Monoids que me ha desconcertado.

Dadas estas definiciones:

data Tree a = Empty | Node a (Tree a) (Tree a) deriving (Show, Read, Eq) instance F.Foldable Tree where foldMap f Empty = mempty foldMap f (Node x l r) = F.foldMap f l `mappend` f x `mappend` F.foldMap f r

Y este árbol:

testTree = Node 5 (Node 3 (Node 1 Empty Empty) (Node 6 Empty Empty) ) (Node 9 (Node 8 Empty Empty) (Node 10 Empty Empty) )

Si corro:

ghci> F.foldl (+) 0 testTree 42 ghci> F.foldl (*) 1 testTree 64800

¿Cómo sabe GHCi qué Monoide usar para el mappend cuando se pliega? Porque, de forma predeterminada, los números en el árbol son solo del tipo Num, y nunca dijimos explícitamente dónde estaban algunos de los Monoides como Suma o Producto.

Entonces, ¿cómo GHCi infiere el Monoide correcto para usar? ¿O estoy totalmente fuera en este punto?

Fuente de ejemplo: http://learnyouahaskell.com/functors-applicative-functors-and-monoids#monoids


No es necesario. foldl se traduce en foldr que se traduce en foldMap sobre Endo que significa composición de la función, lo que significa un simple anidamiento de la función que ha suministrado .

O algo. Es decir, foldl podría traducirse a foldMap sobre Dual . Endo Dual . Endo que compone de izquierda a derecha, etc.

actualización: sí, la documentación dice :

Se espera que las instancias plegables cumplan las siguientes leyes:

foldr f z t = appEndo (foldMap (Endo . f) t ) z foldl f z t = appEndo (getDual (foldMap (Dual . Endo . flip f) t)) z -- << -- fold = foldMap id

Dual (Endo f) <> Dual (Endo g) = Dual (Endo g <> Endo f) = Dual (Endo (g . f)) . Entonces, cuando appEndo ataca, la cadena de funciones que se ha construido, es decir,

((+10) . (+9) . (+8) . (+5) . ... . (+1))

o equivalente (aquí, que se muestra para el caso (+) ), se aplica al valor proporcionado por el usuario, en su caso,

0

Otra cosa a tener en cuenta es que Endo y Dual son newtype , por lo que todas estas maquinaciones las realizará el compilador y desaparecerán en el tiempo de ejecución.


Respuesta corta : es una restricción de tipo en la firma de foldMap .

Si nos fijamos en el código fuente de Foldable (más específicamente foldMap ), vemos:

class Foldable (t :: * -> *) where ... foldMap :: Monoid m => (a -> m) -> t a -> m

Entonces, eso significa que si declaramos que Tree es miembro de Foldable (no que Tree tenga el tipo * -> * ), significa que un foldMap se define sobre ese árbol, de manera que: foldMap :: Monoid m => (a -> m) -> Tree a -> m . Entonces, esto significa que el tipo de resultado (y el resultado de la función pasada a foldMap ) m debe ser un Monoid .

Haskell se escribe de forma estática: después del tiempo de compilación, Haskell sabe exactamente los tipos que se pasan en una instancia de cada función. Eso significa que sabe, por ejemplo, cuál será el tipo de salida y, por lo tanto, cómo manejarlo.

Ahora Int no es una instancia de Monoid . Aquí usa F.foldl (+) 0 testTree , lo que significa que más o menos ha construido un monoide "ad hoc". Esto funciona si nos fijamos en el código fuente de foldl :

foldl :: (b -> a -> b) -> b -> t a -> b foldl f z t = appEndo (getDual (foldMap (Dual . Endo . flip f) t)) z

Esto tiene mucha lógica alrededor de los parámetros f , z y t . Así que vamos a romper eso primero.

Primero echemos un vistazo a Dual . Endo . flip f Dual . Endo . flip f Dual . Endo . flip f . Esto es corto para:

helper = /x -> Dual (Endo (/y -> f y x))

Dual y Endo son tipos con cada constructor que toma un parámetro. Así que fyx el resultado de fyx en los constructores Dual (Endo ...) .

Usaremos esto como la función de foldMap . Si nuestra f tiene el tipo a -> b -> a , entonces esta función tiene el tipo b -> Dual (Endo a) . Por lo tanto, el tipo de salida de la función pasada a foldMap tiene un tipo de salida Dual (Endo a) . Ahora, si inspeccionamos el código fuente, vemos dos cosas interesantes:

instance Monoid (Endo a) where mempty = Endo id Endo f `mappend` Endo g = Endo (f . g) instance Monoid a => Monoid (Dual a) where mempty = Dual mempty Dual x `mappend` Dual y = Dual (y `mappend` x)

(tenga en cuenta que es y `mappend` x , no x `mappend` y ).

Entonces, lo que sucede aquí es que el mempty que se usa en foldMap es mempty = Dual mempty = Dual (Endo id) . Así que un Dual (Endo ...) que encapsula la función de identidad .

Además, el mappend de dos duales se reduce a la composición de la función de los valores en Endo . Asi que:

mempty = Dual (Endo id) mappend (Dual (Endo f)) (Dual (Endo g)) = Dual (Endo (g . f))

Así que eso significa que si nos plegamos sobre el árbol, en caso de que veamos un Empty (una hoja), devolveremos mempty , y en caso de que veamos un Node xlr , realizaremos el mappend como se describe anteriormente. Así que un foldMap " especializado " se verá así:

-- specialized foldMap foldMap f Empty = Dual (Endo id) foldMap f (Node x l r) = Dual (Endo (c . b . a)) where Dual (Endo a) = foldMap f l Dual (Endo b) = helper x Dual (Endo c) = foldMap f l

Entonces, para cada Node hacemos una composición de función de derecha a izquierda sobre los elementos secundarios y el elemento del nodo. a y c pueden ser composiciones del árbol (ya que estas son llamadas recursivas). En el caso de una Leaf , no hacemos nada (devolvemos id , pero la composición sobre una id es un no-op).

Entonces eso significa que si tenemos un árbol:

5 |- 3 | |- 1 | `- 6 `- 9 |- 8 `- 10

Esto resultará en una función:

(Dual (Endo ( (/x -> f x 10) . (/x -> f x 9) . (/x -> f x 8) . (/x -> f x 5) . (/x -> f x 6) . (/x -> f x 3) . (/x -> f x 1) ) ) )

(Se omiten las identidades, para hacerla más limpia). Este es el resultado de getDual (foldMap (Dual . Endo . flip f)) . Pero ahora tenemos que postprocesar este resultado. Con getDual , obtenemos el contenido envuelto en el constructor Dual . Así que ahora tenemos:

Endo ( (/x -> f x 10) . (/x -> f x 9) . (/x -> f x 8) . (/x -> f x 5) . (/x -> f x 6) . (/x -> f x 3) . (/x -> f x 1) )

y con appEndo , obtenemos la función envuelta en Endo , por lo que:

( (/x -> f x 10) . (/x -> f x 9) . (/x -> f x 8) . (/x -> f x 5) . (/x -> f x 6) . (/x -> f x 3) . (/x -> f x 1) )

y luego aplicamos esto a z el valor "inicial". Eso significa que procesaremos la cadena comenzando con z (el elemento inicial) y la aplicaremos como:

f (f (f (f (f (f (f z 1) 3) 6) 5) 8) 9) 10

Así que hemos construido algún tipo de Monoide, donde mappend es reemplazado por f , y mempty como no-op (la función de identidad).