La mónada lectora es tan compleja y parece inútil. En un lenguaje imperativo como Java o C ++, no existe un concepto equivalente para la mónada del lector, si no me equivoco.
¿Puedes darme un ejemplo sencillo y aclarar esto un poco?
La mónada lectora es tan compleja y parece inútil. En un lenguaje imperativo como Java o C ++, no existe un concepto equivalente para la mónada del lector, si no me equivoco.
¿Puedes darme un ejemplo sencillo y aclarar esto un poco?
Respuestas:
¡No tengas miedo! La mónada del lector en realidad no es tan complicada y tiene una utilidad realmente fácil de usar.
Hay dos formas de abordar una mónada: podemos preguntar
Desde el primer enfoque, la mónada lectora es un tipo abstracto
data Reader env a
tal que
-- Reader is a monad
instance Monad (Reader env)
-- and we have a function to get its environment
ask :: Reader env env
-- finally, we can run a Reader
runReader :: Reader env a -> env -> a
Entonces, ¿cómo usamos esto? Bueno, la mónada lectora es buena para pasar información de configuración (implícita) a través de un cálculo.
Siempre que tenga una "constante" en un cálculo que necesite en varios puntos, pero realmente le gustaría poder realizar el mismo cálculo con diferentes valores, entonces debería usar una mónada de lectura.
Las mónadas lectoras también se utilizan para hacer lo que la gente de OO llama inyección de dependencia . Por ejemplo, el algoritmo negamax se usa con frecuencia (en formas altamente optimizadas) para calcular el valor de una posición en un juego de dos jugadores. Sin embargo, al algoritmo en sí no le importa en qué juego estás jugando, excepto que debes poder determinar cuáles son las posiciones "siguientes" en el juego, y debes poder saber si la posición actual es una posición de victoria.
import Control.Monad.Reader
data GameState = NotOver | FirstPlayerWin | SecondPlayerWin | Tie
data Game position
= Game {
getNext :: position -> [position],
getState :: position -> GameState
}
getNext' :: position -> Reader (Game position) [position]
getNext' position
= do game <- ask
return $ getNext game position
getState' :: position -> Reader (Game position) GameState
getState' position
= do game <- ask
return $ getState game position
negamax :: Double -> position -> Reader (Game position) Double
negamax color position
= do state <- getState' position
case state of
FirstPlayerWin -> return color
SecondPlayerWin -> return $ negate color
Tie -> return 0
NotOver -> do possible <- getNext' position
values <- mapM ((liftM negate) . negamax (negate color)) possible
return $ maximum values
Esto funcionará con cualquier juego de dos jugadores finito y determinista.
Este patrón es útil incluso para cosas que no son realmente una inyección de dependencia. Suponga que trabaja en finanzas, podría diseñar una lógica complicada para fijar el precio de un activo (por ejemplo, un derivado), lo cual está muy bien y puede hacerlo sin mónadas apestosas. Pero luego, modifica su programa para manejar múltiples monedas. Necesita poder convertir entre monedas sobre la marcha. Su primer intento es definir una función de nivel superior
type CurrencyDict = Map CurrencyName Dollars
currencyDict :: CurrencyDict
para obtener precios al contado. A continuación, puede llamar a este diccionario en su código ... ¡pero espere! ¡Eso no funcionará! El diccionario de divisas es inmutable y, por lo tanto, debe ser el mismo no solo durante la vida útil de su programa, ¡sino desde el momento en que se compila ! Entonces, ¿Qué haces? Bueno, una opción sería usar la mónada Reader:
computePrice :: Reader CurrencyDict Dollars
computePrice
= do currencyDict <- ask
--insert computation here
Quizás el caso de uso más clásico es la implementación de intérpretes. Pero, antes de ver eso, necesitamos introducir otra función
local :: (env -> env) -> Reader env a -> Reader env a
Bien, entonces Haskell y otros lenguajes funcionales se basan en el cálculo lambda . El cálculo lambda tiene una sintaxis similar a
data Term = Apply Term Term | Lambda String Term | Var Term deriving (Show)
y queremos escribir un evaluador para este idioma. Para hacerlo, necesitaremos realizar un seguimiento de un entorno, que es una lista de enlaces asociados con términos (en realidad, serán cierres porque queremos hacer un alcance estático).
newtype Env = Env ([(String, Closure)])
type Closure = (Term, Env)
Cuando hayamos terminado, deberíamos obtener un valor (o un error):
data Value = Lam String Closure | Failure String
Entonces, escribamos el intérprete:
interp' :: Term -> Reader Env Value
--when we have a lambda term, we can just return it
interp' (Lambda nv t)
= do env <- ask
return $ Lam nv (t, env)
--when we run into a value, we look it up in the environment
interp' (Var v)
= do (Env env) <- ask
case lookup (show v) env of
-- if it is not in the environment we have a problem
Nothing -> return . Failure $ "unbound variable: " ++ (show v)
-- if it is in the environment, then we should interpret it
Just (term, env) -> local (const env) $ interp' term
--the complicated case is an application
interp' (Apply t1 t2)
= do v1 <- interp' t1
case v1 of
Failure s -> return (Failure s)
Lam nv clos -> local (\(Env ls) -> Env ((nv, clos) : ls)) $ interp' t2
--I guess not that complicated!
Finalmente, podemos usarlo pasando un entorno trivial:
interp :: Term -> Value
interp term = runReader (interp' term) (Env [])
Y eso es todo. Un intérprete completamente funcional para el cálculo lambda.
La otra forma de pensar en esto es preguntarse: ¿Cómo se implementa? La respuesta es que la mónada lectora es en realidad una de las mónadas más simples y elegantes.
newtype Reader env a = Reader {runReader :: env -> a}
¡Lector es solo un nombre elegante para funciones! Ya lo hemos definido, runReader
entonces, ¿qué pasa con las otras partes de la API? Bueno, cada Monad
es también un Functor
:
instance Functor (Reader env) where
fmap f (Reader g) = Reader $ f . g
Ahora, para obtener una mónada:
instance Monad (Reader env) where
return x = Reader (\_ -> x)
(Reader f) >>= g = Reader $ \x -> runReader (g (f x)) x
que no da tanto miedo. ask
es realmente simple:
ask = Reader $ \x -> x
mientras local
que no es tan malo:
local f (Reader g) = Reader $ \x -> runReader g (f x)
Bien, entonces la mónada del lector es solo una función. ¿Por qué tiene Reader en absoluto? Buena pregunta. En realidad, ¡no lo necesitas!
instance Functor ((->) env) where
fmap = (.)
instance Monad ((->) env) where
return = const
f >>= g = \x -> g (f x) x
Estos son aún más simples. Además, ask
es justa id
y local
es sólo composición de funciones con el orden de las funciones cambiado.
Reader
es una función con alguna implementación particular de la clase de tipo mónada? Decirlo antes me habría ayudado a desconcertarme un poco menos. Primero no lo entendía. A mitad de camino pensé "Oh, te permite devolver algo que te dará el resultado deseado una vez que proporciones el valor faltante". Pensé que era útil, pero de repente me di cuenta de que una función hace exactamente esto.
local
embargo, la función necesita más explicación ..
(Reader f) >>= g = (g (f x))
?
x
?
Recuerdo estar desconcertado como tú, hasta que descubrí por mi cuenta que las variantes de la mónada Lectora están en todas partes. . ¿Cómo lo descubrí? Porque seguí escribiendo código que resultó ser pequeñas variaciones.
Por ejemplo, en un momento estaba escribiendo un código para tratar con valores históricos ; valores que cambian con el tiempo. Un modelo muy simple de esto son las funciones desde puntos de tiempo hasta el valor en ese momento:
import Control.Applicative
-- | A History with timeline type t and value type a.
newtype History t a = History { observe :: t -> a }
instance Functor (History t) where
-- Apply a function to the contents of a historical value
fmap f hist = History (f . observe hist)
instance Applicative (History t) where
-- A "pure" History is one that has the same value at all points in time
pure = History . const
-- This applies a function that changes over time to a value that also
-- changes, by observing both at the same point in time.
ff <*> fx = History $ \t -> (observe ff t) (observe fx t)
instance Monad (History t) where
return = pure
ma >>= f = History $ \t -> observe (f (observe ma t)) t
La Applicative
instancia significa que si tiene employees :: History Day [Person]
y customers :: History Day [Person]
puede hacer esto:
-- | For any given day, the list of employees followed by the customers
employeesAndCustomers :: History Day [Person]
employeesAndCustomers = (++) <$> employees <*> customers
Es decir, Functor
y Applicative
nos permiten adaptar funciones regulares, no históricas, para trabajar con historias.
La instancia de la mónada se entiende más intuitivamente considerando la función (>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c
. Una función de tipo a -> History t b
es una función que asigna una a
a un historial de b
valores; por ejemplo, podría tener getSupervisor :: Person -> History Day Supervisor
y getVP :: Supervisor -> History Day VP
. Entonces, la instancia de Monad para History
se trata de componer funciones como estas; por ejemplo, getSupervisor >=> getVP :: Person -> History Day VP
es la función que obtiene, para cualquiera Person
, el historial de VP
correos electrónicos que ha tenido.
Bueno, esta History
mónada es exactamente igual que Reader
. History t a
es realmente lo mismo que Reader t a
(que es lo mismo que t -> a
).
Otro ejemplo: he estado creando prototipos de diseños OLAP en Haskell recientemente. Una idea aquí es la de un "hipercubo", que es un mapeo de las intersecciones de un conjunto de dimensiones a valores. Aquí vamos de nuevo:
newtype Hypercube intersection value = Hypercube { get :: intersection -> value }
Una operación común en hipercubos es aplicar funciones escalares de múltiples lugares a los puntos correspondientes de un hipercubo. Esto lo podemos conseguir definiendo una Applicative
instancia para Hypercube
:
instance Functor (Hypercube intersection) where
fmap f cube = Hypercube (f . get cube)
instance Applicative (Hypercube intersection) where
-- A "pure" Hypercube is one that has the same value at all intersections
pure = Hypercube . const
-- Apply each function in the @ff@ hypercube to its corresponding point
-- in @fx@.
ff <*> fx = Hypercube $ \x -> (get ff x) (get fx x)
Acabo de copiar el History
código anterior y cambiar los nombres. Como puedes ver, Hypercube
también es justo Reader
.
Lo sigue y sigue. Por ejemplo, los intérpretes de idiomas también se reducen a Reader
, cuando aplica este modelo:
Reader
ask
Reader
entorno de ejecución.local
Una buena analogía es que a Reader r a
representa una a
con "agujeros" que le impiden saber de qué a
estamos hablando. Solo puede obtener un real a
una vez que proporcione un r
para rellenar los agujeros. Hay toneladas de cosas así. En los ejemplos anteriores, un "historial" es un valor que no se puede calcular hasta que se especifica una hora, un hipercubo es un valor que no se puede calcular hasta que se especifica una intersección y una expresión de lenguaje es un valor que se puede No se calculará hasta que proporcione los valores de las variables. También te da una idea de por qué Reader r a
es lo mismo que r -> a
, porque esa función también es intuitivamente una a
falta r
.
Por lo tanto Functor
, las instancias Applicative
y Monad
de Reader
son una generalización muy útil para los casos en los que modela algo del tipo " a
y le falta un r
" y le permite tratar estos objetos "incompletos" como si estuvieran completos.
Otra forma más de decir lo mismo: a Reader r a
es algo que consume r
y produce a
, y las instancias Functor
, Applicative
y Monad
son patrones básicos para trabajar con Reader
s. Functor
= hacer una Reader
que modifique la salida de otra Reader
; Applicative
= conectar dos Reader
sa la misma entrada y combinar sus salidas; Monad
= inspeccionar el resultado de a Reader
y usarlo para construir otro Reader
. Las funciones local
y withReader
= hacen una Reader
que modifica la entrada a otra Reader
.
GeneralizedNewtypeDeriving
extensión para derivar Functor
, Applicative
, Monad
, etc., para Newtypes en función de sus tipos subyacentes.
En Java o C ++ puedes acceder a cualquier variable desde cualquier lugar sin ningún problema. Aparecen problemas cuando su código se vuelve multiproceso.
En Haskell, solo tiene dos formas de pasar el valor de una función a otra:
fn1 -> fn2 -> fn3
, la función fn2
puede no necesitar parámetro que se pasa de fn1
a fn3
.La mónada Reader simplemente pasa los datos que desea compartir entre funciones. Las funciones pueden leer esos datos, pero no pueden cambiarlos. Eso es todo lo que hace la mónada Reader. Bueno, casi todos. También hay una serie de funciones como local
, pero por primera vez solo puedes quedarte asks
.
do
anotaciones de código de 'estilo imperativo' , que sería mejor refactorizar en una función pura.
where
cláusula, ¿se aceptará como una tercera forma de pasar variables?