haskell - monad - A diferencia de un Functor, ¿una Mónada puede cambiar de forma?
monad example (8)
Siempre he disfrutado la siguiente explicación intuitiva del poder de una mónada en relación con un functor: una mónada puede cambiar de forma; un funtor no puede.
Por ejemplo: length $ fmap f [1,2,3]
siempre es igual a 3
.
Sin embargo, con una mónada, la length $ [1,2,3] >>= g
a menudo no es igual a 3
. Por ejemplo, si g
se define como:
g :: (Num a) => a -> [a]
g x = if x==2 then [] else [x]
entonces [1,2,3] >>= g
es igual a [1,3]
.
Lo que me preocupa un poco, es el tipo de firma de g
. Parece imposible definir una función que cambie la forma de la entrada, con un tipo monádico genérico como:
h :: (Monad m, Num a) => a -> m a
Las clases de tipo MonadPlus o MonadZero tienen elementos cero relevantes, para usar en lugar de []
, pero ahora tenemos algo más que una mónada.
¿Estoy en lo correcto? Si es así, ¿hay una manera de expresar esta sutileza a un recién llegado a Haskell? Me gustaría hacer que mi amada frase "las mónadas pueden cambiar de forma", solo un poco más honesta; si es necesario.
Siempre he disfrutado la siguiente explicación intuitiva del poder de una mónada en relación con un functor: una mónada puede cambiar de forma; un funtor no puede.
Te estás perdiendo un poco de sutileza aquí, por cierto. En aras de la terminología, dividiré un Functor
en el sentido de Haskell en tres partes: el componente paramétrico determinado por el parámetro de tipo y operado por fmap
, las partes que no fmap
, como el constructor de tuplas en State
y la "forma" como cualquier otra cosa, como las elecciones entre los constructores (por ejemplo, Nothing
vs. Just
) o partes que involucran otros parámetros de tipo (por ejemplo, el entorno en Reader
).
Un Functor
solo está limitado a funciones de mapeo sobre la parte paramétrica, por supuesto.
Una Monad
puede crear nuevas "formas" basadas en los valores de la parte paramétrica , lo que permite mucho más que simplemente cambiar formas. Duplicar todos los elementos de una lista o eliminar los primeros cinco elementos cambiaría la forma, pero filtrar una lista requiere inspeccionar los elementos.
Esta es esencialmente la forma en que el Applicative
ajusta entre ellos: le permite combinar las formas y los valores paramétricos de dos Functors
independiente, sin dejar que estos últimos influyan en los primeros.
¿Estoy en lo correcto? Si es así, ¿hay una manera de expresar esta sutileza a un recién llegado a Haskell? Me gustaría hacer que mi amada frase "las mónadas pueden cambiar de forma", solo un poco más honesta; si es necesario.
Quizás la sutileza que estás buscando aquí es que realmente no estás "cambiando" nada. Nada en una Monad
te permite Monad
explícitamente con la forma. Lo que te permite hacer es crear nuevas formas basadas en cada valor paramétrico y hacer que esas nuevas formas se vuelvan a combinar en una nueva forma compuesta.
Por lo tanto, siempre estará limitado por las formas disponibles para crear formas. Con una Monad
completamente genérica, todo lo que tiene es return
, que por definición crea cualquier forma que sea necesaria de tal manera que (>>= return)
sea la función de identidad. La definición de una Monad
le dice lo que puede hacer, dados ciertos tipos de funciones; no proporciona esas funciones para usted.
El combinador de teclas para las mónadas es (>>=)
. Sabiendo que compone dos valores monádicos y leyendo su tipo de firma, el poder de las mónadas se hace más evidente:
(>>=) :: Monad m => m a -> (a -> m b) -> m b
La acción futura puede depender completamente del resultado de la primera acción, porque es una función de su resultado. Sin embargo, este poder tiene un precio: las funciones en Haskell son completamente opacas, por lo que no hay forma de que puedas obtener información sobre una acción compuesta sin ejecutarla. Como nota al margen, aquí es donde entran las flechas.
El hecho de que el patrón de mónada incluya algunas instancias particulares que permitan cambios de forma no significa que cada instancia pueda tener cambios de forma. Por ejemplo, solo hay una "forma" disponible en la mónada de Identity
:
newtype Identity a = Identity a
instance Monad Identity where
return = Identity
Identity a >>= f = f a
De hecho, no me queda claro que muchas mónadas tengan "formas" significativas: por ejemplo, ¿qué significa la forma en las mónadas de State
, Reader
, Writer
, ST
, STM
o IO
?
El tipo más simple de una función que satisface el requisito que puedo imaginar es este:
enigma :: Monad m => m () -> m ()
Uno puede implementarlo de una de las siguientes maneras:
enigma1 m = m -- not changing the shape
enigma2 _ = return () -- changing the shape
Este fue un cambio muy simple: enigma2
simplemente descarta la forma y la reemplaza por la trivial. Otro tipo de cambio genérico es combinar dos formas juntas:
foo :: Monad m => m () -> m () -> m ()
foo a b = a >> b
El resultado de foo
puede tener una forma diferente tanto de a
como de b
.
Un tercer cambio obvio de forma, que requiere el poder completo de la mónada, es un
join :: Monad m => m (m a) -> m a
join x = x >>= id
La forma de join x
no suele ser la misma que la de x
.
Combinando esos cambios primitivos de forma, uno puede derivar cosas no triviales como la sequence
, el foldM
y el mismo.
Esta no es una respuesta completa, pero tengo algunas cosas que decir sobre su pregunta que realmente no encajan en un comentario.
En primer lugar, Monad
y Functor
son clases de tipos; clasifican los tipos . Por lo tanto, es extraño decir que "una mónada puede cambiar de forma; un funtor no puede". Creo que está tratando de hablar sobre un "valor monádico" o tal vez una "acción monádica": un valor cuyo tipo es ma
para una Monad m
de tipo * -> *
y algún otro tipo de tipo *
. No estoy completamente seguro de cómo llamar a Functor f :: fa
, supongo que lo llamaría "valor en un functor", aunque esa no es la mejor descripción de, digamos, IO String
( IO
es un functor).
En segundo lugar, tenga en cuenta que todas las mónadas son necesariamente funcionales ( fmap = liftM
), por lo que diría que la diferencia que observa es entre fmap
y >>=
, o incluso entre f
y g
, en lugar de entre Monad
y Functor
.
Hace
h :: (Monad m, Num a) => a -> m a
h 0 = fail "Failed."
h a = return a
satisfacer sus necesidades? Por ejemplo,
> [0,1,2,3] >>= h
[1,2,3]
Las operaciones de la Monad
pueden "cambiar la forma" de los valores en la medida en que la función >>=
reemplaza los nodos de hoja en el "árbol", que es el valor original con una nueva subestructura derivada del valor del nodo (para una noción general adecuada de "árbol" - en el caso de la lista, el "árbol" es asociativo).
En su ejemplo de lista, lo que está sucediendo es que cada número (hoja) está siendo reemplazado por la nueva lista que resulta cuando g
se aplica a ese número. La estructura general de la lista original aún se puede ver si sabe lo que está buscando; los resultados de g
todavía están en orden, simplemente se rompieron juntos por lo que no se puede decir dónde termina uno y comienza el siguiente, a menos que ya lo sepa.
Un punto de vista más esclarecedor puede ser considerar fmap
y join
lugar de >>=
. Junto con el return
, de cualquier manera da una definición equivalente de una mónada. fmap
embargo, en la vista fmap
/ join
, lo que está sucediendo aquí es más claro. Continuando con el ejemplo de su lista, la primera g
es fmap
ped sobre la lista que produce [[1],[],[3]]
. Entonces esa lista es join
ed, que para la lista es solo concat
.
Una función con una firma como h
no puede hacer muchas cosas interesantes más allá de realizar una cierta aritmética en su argumento. Entonces, tienes la intuición correcta allí.
Sin embargo, podría ser útil buscar en las bibliotecas más utilizadas las funciones con firmas similares . Encontrará que los más genéricos, como es de esperar, realizan operaciones de mónada genéricas como return
, liftM
o join
. Además, cuando usa liftM
o fmap
para elevar una función ordinaria a una función monádica, normalmente termina con una firma genérica similar, y esto es muy conveniente para integrar funciones puras con código monádico.
Para utilizar la estructura que ofrece una mónada en particular, inevitablemente es necesario que conozca algo sobre la mónada específica en la que se encuentra para crear nuevos e interesantes cálculos en esa mónada. Considere la mónada estatal, (s -> (a, s))
. Sin conocer ese tipo, no podemos escribir get = /s -> (s, s)
, pero sin poder acceder al estado, no tiene mucho sentido estar en la mónada.