The reader monad is so complex and seems to be useless. In an imperative language like Java or C++, there is no equivalent concept for the reader monad, if I am not mistaken.
Can you give me a simple example and clear this up a little bit?
The reader monad is so complex and seems to be useless. In an imperative language like Java or C++, there is no equivalent concept for the reader monad, if I am not mistaken.
Can you give me a simple example and clear this up a little bit?
Don't be scared! The reader monad is actually not so complicated, and has real easy-to-use utility.
There are two ways of approaching a monad: we can ask
From the first approach, the reader monad is some abstract type
data Reader env a
such that
-- 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
So how do we use this? Well, the reader monad is good for passing (implicit) configuration information through a computation.
Any time you have a "constant" in a computation that you need at various points, but really you would like to be able to perform the same computation with different values, then you should use a reader monad.
Reader monads are also used to do what the OO people call dependency injection. For example, the negamax algorithm is used frequently (in highly optimized forms) to compute the value of a position in a two player game. The algorithm itself though does not care what game you are playing, except that you need to be able to determine what the "next" positions are in the game, and you need to be able to tell if the current position is a victory position.
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
This will then work with any finite, deterministic, two player game.
This pattern is useful even for things that are not really dependency injection. Suppose you work in finance, you might design some complicated logic for pricing an asset (a derivative say), which is all well and good and you can do without any stinking monads. But then, you modify your program to deal with multiple currencies. You need to be able to convert between currencies on the fly. Your first attempt is to define a top level function
type CurrencyDict = Map CurrencyName Dollars
currencyDict :: CurrencyDict
to get spot prices. You can then call this dictionary in your code....but wait! That won't work! The currency dictionary is immutable and so has to be the same not only for the life of your program, but from the time it gets compiled! So what do you do? Well, one option would be to use the Reader monad:
computePrice :: Reader CurrencyDict Dollars
computePrice
= do currencyDict <- ask
--insert computation here
Perhaps the most classic use-case is in implementing interpreters. But, before we look at that, we need to introduce another function
local :: (env -> env) -> Reader env a -> Reader env a
Okay, so Haskell and other functional languages are based on the lambda calculus. Lambda calculus has a syntax that looks like
data Term = Apply Term Term | Lambda String Term | Var Term deriving (Show)
and we want to write an evaluator for this language. To do so, we will need to keep track of an environment, which is a list of bindings associated with terms (actually it will be closures because we want to do static scoping).
newtype Env = Env ([(String, Closure)])
type Closure = (Term, Env)
When we are done, we should get out a value (or an error):
data Value = Lam String Closure | Failure String
So, let's write the interpreter:
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!
Finally, we can use it by passing a trivial environment:
interp :: Term -> Value
interp term = runReader (interp' term) (Env [])
And that is it. A fully functional interpreter for the lambda calculus.
The other way to think about this is to ask: How is it implemented? The answer is that the reader monad is actually one of the simplest and most elegant of all monads.
newtype Reader env a = Reader {runReader :: env -> a}
Reader is just a fancy name for functions! We have already defined runReader
so what about the other parts of the API? Well, every Monad
is also a Functor
:
instance Functor (Reader env) where
fmap f (Reader g) = Reader $ f . g
Now, to get a monad:
instance Monad (Reader env) where
return x = Reader (\_ -> x)
(Reader f) >>= g = Reader $ \x -> runReader (g (f x)) x
which is not so scary. ask
is really simple:
ask = Reader $ \x -> x
while local
isn't so bad:
local f (Reader g) = Reader $ \x -> runReader g (f x)
Okay, so the reader monad is just a function. Why have Reader at all? Good question. Actually, you don't need it!
instance Functor ((->) env) where
fmap = (.)
instance Monad ((->) env) where
return = const
f >>= g = \x -> g (f x) x
These are even simpler. What's more, ask
is just id
and local
is just function composition with the order of the functions switched!
Reader
is a function with some particular implementation of the monad type class? Saying it earlier would have helped me being puzzled a bit less. First I wasn't getting it. Half way through I thought "Oh, it allows you to return something that will give you the desired result once you supply the missing value." I thought that's useful, but suddenly realized that a function does exactly this. –
Contradistinguish local
function does need some more explanation though.. –
Miletus (Reader f) >>= g = (g (f x))
? –
Brassbound x
? –
Brehm local
, though. It should be local f (Reader g) = Reader $ \x -> g (f x)
. The runReader
isn't needed since g
is extracted in the pattern match. –
Knockout I remember being puzzled as you were, until I discovered on my own that variants of the Reader monad are everywhere. How did I discover it? Because I kept writing code that turned out to be small variations on it.
For example, at one point I was writing some code to deal with historical values; values that change over time. A very simple model of this is functions from points of time to the value at that point in time:
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
The Applicative
instance means that if you have employees :: History Day [Person]
and customers :: History Day [Person]
you can do this:
-- | For any given day, the list of employees followed by the customers
employeesAndCustomers :: History Day [Person]
employeesAndCustomers = (++) <$> employees <*> customers
I.e., Functor
and Applicative
allow us to adapt regular, non-historical functions to work with histories.
The monad instance is most intuitively understood by considering the function (>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c
. A function of type a -> History t b
is a function that maps an a
to a history of b
values; for example, you could have getSupervisor :: Person -> History Day Supervisor
, and getVP :: Supervisor -> History Day VP
. So the Monad instance for History
is about composing functions like these; for example, getSupervisor >=> getVP :: Person -> History Day VP
is the function that gets, for any Person
, the history of VP
s that they've had.
Well, this History
monad is actually exactly the same as Reader
. History t a
is really the same as Reader t a
(which is the same as t -> a
).
Another example: I've been prototyping OLAP designs in Haskell recently. One idea here is that of a "hypercube," which is a mapping from intersections of a set of dimensions to values. Here we go again:
newtype Hypercube intersection value = Hypercube { get :: intersection -> value }
One common of operation on hypercubes is to apply a multi-place scalar functions to corresponding points of a hypercube. This we can get by defining an Applicative
instance for 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)
I just copypasted the History
code above and changed names. As you can tell, Hypercube
is also just Reader
.
It goes on and on. For example, language interpreters also boil down to Reader
, when you apply this model:
Reader
ask
Reader
execution environment.local
A good analogy is that a Reader r a
represents an a
with "holes" in it, that prevent you from knowing which a
we're talking about. You can only get an actual a
once you supply a an r
to fill in the holes. There are tons of things like that. In the examples above, a "history" is a value that can't be computed until you specify a time, a hypercube is a value that can't be computed until you specify an intersection, and a language expression is a value that can't be computed until you supply the values of the variables. It also gives you an intuition on why Reader r a
is the same as r -> a
, because such a function is also intuitively an a
missing an r
.
So the Functor
, Applicative
and Monad
instances of Reader
are a very useful generalization for cases where you are modeling anything of the sort "an a
that's missing an r
," and allow you to treat these "incomplete" objects as if they were complete.
Yet another way of saying the same thing: a Reader r a
is something that consumes r
and produces a
, and the Functor
, Applicative
and Monad
instances are basic patterns for working with Reader
s. Functor
= make a Reader
that modifies the output of another Reader
; Applicative
= connect two Reader
s to the same input and combine their outputs; Monad
= inspect the result of a Reader
and use it to construct another Reader
. The local
and withReader
functions = make a Reader
that modifies the input to another Reader
.
GeneralizedNewtypeDeriving
extension to derive Functor
, Applicative
, Monad
, etc. for newtypes based on their underlying types. –
Innutrition In Java or C++ you may access any variable from anywhere without any problem. Problems appears when your code becomes multi-threaded.
In Haskell you have only two ways to pass the value from one function to another:
fn1 -> fn2 -> fn3
, function fn2
may not need parameter which you pass from fn1
to fn3
.The Reader monad just pass the data you want to share between functions. Functions may read that data, but can't change it. That's all that do the Reader monad. Well, almost all. There are also number of functions like local
, but for the first time you can stick with asks
only.
do
-notation, which would be better off being refactored into a pure function. –
Beauchamp where
clause, will it be accepted as a 3rd way to pass variables? –
Leaseholder I have learned much from the above answers, and thanks them for answering. Although I'm new to Haskell, I want to say something about the Reader
monad.
First, treating a Reader as a Computation
is important. It's not a state, but a computation.
For example, the function calc_isCountCorrect
mentioned in first official example of Reader monad returns a Reader Bindings Bool
, which means it receives a Bindings
when runReader
, and return a Bool. It's a computation.
calc_isCountCorrect :: Reader Bindings Bool
calc_isCountCorrect = do
count <- asks (lookupVar "count")
bindings <- ask
return (count == (Map.size bindings))
You can also pass the Bindings
through an argument, there is no significant differences when your code is quite simple.
calcIsCountCorrectWithoutReader :: Bindings -> Bool
calcIsCountCorrectWithoutReader bindings = do
let num = lookupVar "count" bindings
let count = Map.size bindings
num == count
However, it does have a difference, that's where the value comes from
. It gives you a way to retrieve it by an implicit source instead of an argument.
When it comes to the question of analogy, I think the lambda
in C++
is a good explanation.
In an imperative language like Java or C++, there is no equivalent concept for the reader monad.
The reader gives you abilities to retrieve a value from outside(NOT global variable, but an upper scope). It's pretty like a capture clause in C++ lambda
.
For example, you have a haskell code that trebles number
and adds it with an implicit value. The following code outputs 10
.
import Control.Monad.Reader
trebleNumberAndAddImplicitly :: Int -> Reader Int Int
trebleNumberAndAddImplicitly number = do
implicitNumber <- ask
return $ 3*number + implicitNumber
main :: IO ()
main = do
print $ runReader (trebleNumberAndAddImplicitly 3) 1
The implicit value is outside the COMPUTATION but accessible with Reader
.
Inside C++, this is called capture clause
. The output is: output is: 10
. However, it has more limitations than haskell. But it's similar in my mind.
#include<iostream>
int main(){
int implicitNumber = 1;
auto trebleNumberAndAddImplicitly = [implicitNumber](int number) -> int {
return (3*number + implicitNumber);
};
std::cout << "output is: " << trebleNumberAndAddImplicitly(3);
}
© 2022 - 2024 — McMap. All rights reserved.