haskell monads reader-monad

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ón fn1 -> fn2 -> fn3 puede no necesitar el parámetro que pasa de fn1 a fn3 .
  • 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

  1. ¿Qué hace la mónada? ¿Con qué operaciones está equipado? ¿Para que sirve?
  2. ¿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 .