haskell - ¿Cuál es el propósito de Reader Monad?
monads reader-monad (3)
En Java o C ++ puede acceder a cualquier variable desde cualquier lugar sin ningún problema. Los problemas aparecen cuando su código se convierte en multiproceso.
En Haskell solo tienes dos formas de pasar el valor de una función a otra:
- Pasa el valor a través de uno de los parámetros de entrada de la función invocable. Los inconvenientes son: 1) no se pueden pasar TODAS las variables de esa manera; la lista de parámetros de entrada simplemente te deja sin habla. 2) en la secuencia de llamadas a funciones:
fn1 -> fn2 -> fn3
, la funciónfn1 -> fn2 -> fn3
puede no necesitar el parámetro que pasa defn1
afn3
. - Usted pasa el valor en el alcance de alguna mónada. El inconveniente es: tienes que entender con firmeza qué es la concepción de la mónada. Aprobar los valores es solo una de las muchas aplicaciones en las que puede usar las Mónadas. En realidad, la concepción de la Mónada es increíblemente poderosa. No te enfades si no obtuviste una idea a la vez. Solo sigue intentando y lee diferentes tutoriales. El conocimiento que obtendrá valdrá la pena.
La mónada Reader acaba de pasar 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 todo. También hay varias funciones como local
, pero por primera vez puede seguir con las asks
solamente.
El Reader Monad es tan complejo y parece ser inútil. En lenguaje imperativo como Java o C ++, no hay un término equivalente para mónada de lector (si estoy en lo cierto).
¿Puedes darme un ejemplo simple y aclararme un poco?
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 acercarse a una mónada: podemos preguntar
- ¿Qué hace la mónada? ¿Con qué operaciones está equipado? ¿Para que sirve?
- ¿Cómo se implementa la mónada? ¿De dónde surge?
Desde el primer enfoque, la mónada del lector 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 del lector es buena para pasar información (implícita) de configuración a través de un cálculo.
Cada vez que tenga una "constante" en un cálculo que necesita en varios puntos, pero en realidad le gustaría poder realizar el mismo cálculo con diferentes valores, entonces debe usar una mónada de lector.
Las mónadas Reader también se usan para hacer lo que las personas OO llaman inyección de dependencia . Por ejemplo, el algoritmo negamax se usa frecuentemente (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 qué juego estás jugando, excepto que necesitas ser capaz de determinar cuáles son las "próximas" posiciones en el juego, y necesitas poder decir 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 finito, determinista y de dos jugadores.
Este patrón es útil incluso para cosas que no son realmente inyección de dependencia. Supongamos que usted trabaja en finanzas, puede diseñar una lógica complicada para fijar el precio de un activo (un derivado), que está muy bien y puede prescindir de cualquier mónada maloliente. Pero luego, modificas tu programa para tratar con múltiples monedas. Necesita poder convertir monedas al vuelo. 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 tiene que ser el mismo no solo durante la vida 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
Tal vez el caso de uso más clásico es la implementación de intérpretes. Pero, antes de ver eso, tenemos que introducir otra función
local :: (env -> env) -> Reader env a -> Reader env a
De acuerdo, entonces Haskell y otros lenguajes funcionales se basan en el cálculo lambda . El cálculo lambda tiene una sintaxis que se parece a
data Term = Apply Term Term | Lambda String Term | Var Term deriving (Show)
y queremos escribir un evaluador para este idioma. Para hacerlo, necesitaremos hacer 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 terminemos, 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 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, than 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.
Entonces, la otra forma de pensar sobre esto es preguntar: ¿cómo se implementa? Bueno, la respuesta es que la mónada del lector es en realidad una de las mónadas más simples y elegantes.
newtype Reader env a = Reader {runReader :: env -> a}
¡Reader es solo un nombre elegante para funciones! Ya 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 es tan aterrador. ask
es realmente simple:
ask = Reader $ /x -> x
mientras que local
no es tan malo.
local f (Reader g) = Reader $ /x -> runReader g (f x)
De acuerdo, entonces la mónada del lector es solo una función. ¿Por qué tener 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. Lo que es más, ask
es solo id
y local
es solo composición de funciones en el otro orden.
Recuerdo haber estado desconcertado como tú, hasta que descubrí por mi cuenta que las variantes de la mónada Reader están en todas partes . ¿Cómo lo descubrí? Porque seguí escribiendo código que resultó ser pequeñas variaciones en él.
Por ejemplo, en un momento escribí un código para tratar con valores históricos ; valores que cambian con el tiempo Un modelo muy simple de esto es funciones desde puntos de tiempo hasta el valor en ese punto en el tiempo:
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 instancia Aplicable 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 y no históricas para trabajar con historias.
La instancia de la mónada se entiende más intuitivamente al considerar la función (>=>) :: Monad m => (a -> mb) -> (b -> mc) -> a -> mc
. Una función de tipo a -> History tb
es una función que asigna un a
a un historial de valores b
; por ejemplo, podría tener getSupervisor :: Person -> History Day Supervisor
, y getVP :: Supervisor -> History Day VP
. Entonces, la instancia de Monad para History
trata sobre la composición de funciones como estas; por ejemplo, getSupervisor >=> getVP :: Person -> History Day VP
es la función que obtiene, para cualquier Person
, el historial de VP
que han tenido.
Bueno, esta mónada de History
es exactamente la misma que Reader
. History ta
es realmente lo mismo que Reader ta
(que es lo mismo que t -> a
).
Otro ejemplo: he estado OLAP prototipos de diseños OLAP en Haskell recientemente. Una idea aquí es la de un "hipercubo", que es un mapeo desde 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 obtener definiendo una instancia Hypercube
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 código de History
arriba y cambié los nombres. Como puede ver, Hypercube
también es solo Reader
.
Lo sigue y sigue. Por ejemplo, los intérpretes de idiomas también se reducen a Reader
, cuando aplica este modelo:
- Expresión = un
Reader
- Variables gratuitas = usos de
ask
- Entorno de evaluación = Entorno de ejecución del
Reader
. - Construcciones vinculantes =
local
Una buena analogía es que un Reader ra
representa una a
con "agujeros" en ella, que le impide saber de qué estamos hablando. Solo puede obtener una real a
vez que suministre una 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 puede no se computará hasta que suministre los valores de las variables. También le da una intuición sobre por qué Reader ra
es lo mismo que r -> a
, porque dicha función también es intuitivamente a
falta de r
.
Por lo tanto, las instancias Functor
, Applicative
y Monad
de Reader
son una generalización muy útil para los casos en los que está modelando algo del tipo "an a
que le falta una r
", y le permiten tratar estos objetos "incompletos" como si estuvieran completos.
Otra forma de decir lo mismo: un Reader ra
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
= Functor
un Reader
que modifica la salida de otro Reader
; Applicative
= conecta dos Reader
a la misma entrada y combina sus salidas; Monad
= inspeccionar el resultado de un Reader
y usarlo para construir otro Reader
. Las funciones local
y withReader
= hacen un Reader
que modifica la entrada a otro Reader
.