haskell - monadas - ¿Es mejor definir Functor en términos de Aplicativo en términos de Mónada, o viceversa?
monad haskell example (5)
A menudo elijo un enfoque inverso en comparación con el de la respuesta de Abrahamson. Defino manualmente solo la instancia de Monad
y defino el Applicative
y el Functor
en términos de ello con la ayuda de funciones ya definidas en Control.Monad
, que hace que esas instancias sean iguales para absolutamente cualquier mónada, es decir:
instance Applicative SomeMonad where
pure = return
(<*>) = ap
instance Functore SomeMonad where
fmap = liftM
Si bien de esta manera, la definición de Functor
y Applicative
es siempre "sin cerebro" y muy fácil de razonar, debo señalar que esta no es la solución definitiva, ya que hay casos en los que las instancias se pueden implementar de manera más eficiente o incluso proporcionar nuevas características. Por ejemplo, la instancia Applicative
de Concurrently
ejecuta cosas ... al mismo tiempo, mientras que la instancia de Monad
solo puede ejecutarlas secuencialmente debido a las mónadas naturales.
Esta es una pregunta general, no vinculada a ninguna pieza de código.
Digamos que tienes un tipo T a
que se le puede dar una instancia de Monad
. Dado que cada mónada es un Applicative
asignando pure = return
y (<*>) = ap
, y luego cada aplicativo es un Functor
través de fmap fx = pure f <*> x
, ¿es mejor definir primero su instancia de Monad
, y Entonces, trivialmente, da T
instancias de Applicative
y Functor
?
Se siente un poco hacia atrás para mí. Si estuviera haciendo matemáticas en lugar de programar, creo que primero mostraría que mi objeto es un functor, y luego continuaré agregando restricciones hasta que también muestre que es una mónada. Sé que Haskell está meramente inspirado en la teoría de categorías y obviamente las técnicas que uno usaría cuando construyera una prueba no son las técnicas que usaría al escribir un programa útil, pero me gustaría obtener una opinión de la comunidad de Haskell. ¿Es mejor ir de Monad
a Functor
? ¿O desde Functor
hasta Monad
?
Creo que entiendes mal cómo funcionan las subclases en Haskell. ¡No son como las subclases OO! En cambio, una restricción de subclase, como
class Applicative m => Monad m
dice "cualquier tipo con una estructura de Monad
canónica también debe tener una estructura de Applicative
canónica". Hay dos razones básicas por las que colocarías una restricción como esa:
- La estructura de subclase induce una estructura de superclase .
- La estructura de superclase es un subconjunto natural de la estructura de subclase.
Por ejemplo, considere:
class Vector v where
(.^) :: Double -> v -> v
(+^) :: v -> v -> v
negateV :: v -> v
class Metric a where
distance :: a -> a -> Double
class (Vector v, Metric v) => Norm v where
norm :: v -> Double
La primera restricción de Norm
en Norm
surge porque el concepto de un espacio normado es realmente débil a menos que también asumas una estructura de espacio vectorial; el segundo surge porque (dado un espacio vectorial) una Norm
induce una Metric
, que puede probar observando que
instance Metric V where
distance v0 v1 = norm (v0 .^ negateV v1)
es una instancia de Metric
válida para cualquier V
con una instancia de Vector
válida y una función de norm
válida. Decimos que la norma induce una métrica. Ver http://en.wikipedia.org/wiki/Normed_vector_space#Topological_structure .
Las Functor
y Applicative
en Monad
son como Metric
, no como Vector
: el return
y >>=
funciones de Monad
inducen las estructuras de Functor
y Applicative
:
-
fmap
: se puede definir comofmap fa = a >>= return . f
fmap fa = a >>= return . f
, que fueliftM
en la biblioteca estándar Haskell 98. -
pure
: es la misma operación quereturn
; los dos nombres son un legado de cuandoApplicative
no era una súper clase deMonad
. -
<*>
: se puede definir comoaf <*> ax = af >>= / f -> ax >>= / x -> return (fx)
, que fueliftM2 ($)
en la biblioteca estándar de Haskell 98. -
join
: se puede definir comojoin aa = aa >>= id
.
Así que es perfectamente sensible, matemáticamente, definir las operaciones de Functor
y Applicative
en términos de Monad
.
La magia aquí, que el Haskell usa la notación Kleisli-tiplet de una mónada, es una forma más conveniente, si alguien quiere usar mónadas en la imperativa programación como herramientas.
Hice la misma pregunta, y la respuesta aparece después de un tiempo, si ve las definiciones de Functor , Applicative , Monad en haskell, se pierde un enlace, que es la definición original de la mónada, que contiene solo la operación de unión , que Se puede encontrar en el HaskellWiki .
Con este punto de vista, verá cómo se construyen las mónadas de haskell, el funtor, los funtores aplicativos, las mónadas y el triplete de Kliesli.
Puede encontrar una explicación aproximada aquí: https://github.com/andorp/pearls/blob/master/Monad.hs Y otra con las mismas ideas aquí: http://people.inf.elte.hu/pgj/haskell2/jegyzet/08/Monad.hs
Tiendo a escribir y ver escrito primero la instancia de Functor
. Doblemente es así porque si utiliza el pragma LANGUAGE DeriveFunctor
, los data Foo a = Foo a deriving ( Functor )
funcionan la mayor parte del tiempo.
Los trucos difíciles están relacionados con el acuerdo de los casos en que su Applicative
puede ser más general que su Monad
. Por ejemplo, aquí hay un tipo de datos Err
data Err e a = Err [e] | Ok a deriving ( Functor )
instance Applicative (Err e) where
pure = Ok
Err es <*> Err es'' = Err (es ++ es'')
Err es <*> _ = Err es
_ <*> Err es = Err es
Ok f <*> Ok x = Ok (f x)
instance Monad (Err e) where
return = pure
Err es >>= _ = Err es
Ok a >>= f = f a
Anteriormente, definí las instancias en el orden Functor
to- Monad y, en forma aislada, cada instancia es correcta. Desafortunadamente, las instancias de Applicative
y Monad
no se alinean: ap
y (<*>)
son visiblemente diferentes a como son (>>)
y (*>)
.
Err "hi" <*> Err "bye" == Err "hibye"
Err "hi" `ap` Err "bye" == Err "hi"
Para propósitos de sensibilidad, especialmente una vez que la Propuesta de Solicitud / Mónada esté en manos de todos, estos deben alinearse. Si definió la instance Applicative (Err e) where { pure = return; (<*>) = ap }
instance Applicative (Err e) where { pure = return; (<*>) = ap }
entonces se alinearán.
Pero luego, finalmente, puede ser capaz de separar cuidadosamente las diferencias entre el Applicative
y la Monad
para que se comporten de manera diferente en formas benignas, como tener una instancia del Aplicativo más perezosa o más eficiente. En realidad, esto ocurre con bastante frecuencia y creo que el jurado todavía está un poco fuera de lo que significa "benigno" y en qué tipo de "observación" deben alinearse sus instancias. Quizás parte del uso más gregario de esto está en el proyecto Haxl en Facebook, donde la instancia de Applicative
está más paralelizada que la de Monad
, y por lo tanto es mucho más eficiente a costa de algunos efectos secundarios "no observados" bastante graves.
En cualquier caso, si difieren, documentarlo.
Functor
instancias de Functor
suelen ser muy simples de definir, normalmente las hago a mano.
Para el Applicative
y la Monad
, depende. pure
y de return
suelen ser igualmente fáciles, y realmente no importa en qué clase se ubique la definición expandida. Para el enlace, a veces es benéfico ir a la "forma de categoría", es decir, definir una join'' :: (M (M x)) -> M x
especializada join'' :: (M (M x)) -> M x
primero y luego a>>=b = join'' $ fmap ba
(que por supuesto no funcionaría si hubiera definido fmap
en términos de >>=
). Entonces probablemente sea útil simplemente reutilizar (>>=)
para la instancia del Applicative
.
Otras veces, la instancia de Applicative
puede escribirse con bastante facilidad o es más eficiente que la implementación genérica derivada de Monad . En ese caso, definitivamente debe definir <*>
separado.