What is the purpose of the reader monad?
Asked Answered
G

4

153

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?

Guevara answered 6/1, 2013 at 3:6 Comment(5)
You use the reader monad if you want to - on occasion - read some values from an (unmodifiable) environment, but don't want to explicitly pass that environment around. In Java or C++, you'd use global variables (though it's not exactly the same).Rearrange
@Daniel: That sounds an awful lot like an answerCopulate
@TokenMacGuy Too short for an answer, and it's too late now for me to think up something longer. If nobody else does, I will after I have slept.Rearrange
In Java or C++, the Reader monad would be analogous to configuration parameters passed to an object in its constructor which are never changed during the lifetime of the object. In Clojure, it would be a bit like a dynamically scoped variable used to parametrize the behaviour of a function without needing to pass it explicitly as a parameter.Keim
@DanielFischer I wish you'd make your comment an answer (with a bit more fluff to make it a proper answer). Its probably the most concise and easiest one to quickly grasp.Offside
C
210

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

  1. What does the monad do? What operations is it equipped with? What is it good for?
  2. How is the monad implemented? From where does it arise?

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!

Coexecutor answered 6/1, 2013 at 5:52 Comment(8)
Very interesting answer. Honestly, I read it again many times, when I want to review monad. By the way, about the nagamax algorithm, "values <- mapM (negate . negamax (negate color)) possible" seems not correct. I know that, the code you provide is just for showing how reader monad works. But if you have time, could you correct the code of negamax algorithm? Because, it's interesting, when you use reader monad to solve negamax.Guevara
So 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
After reading this I understand most of it. The local function does need some more explanation though..Miletus
@Philip I have a question about the Monad instance. Can't we write the bind function as (Reader f) >>= g = (g (f x))?Brassbound
@Brassbound where is x ?Brehm
"This will then work with any finite, deterministic, two player game." I am still not clear. Could you show an example game using negamax there? Like maybe Tic-tac-toe?Codification
Working through your answer was really informative. I stumbled on the definition of 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
Is it correct to say that the Reader monad is actually just a fancy continuation passing?Mellifluent
H
67

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 VPs 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:

  • Expression = a Reader
  • Free variables = uses of ask
  • Evaluation environment = Reader execution environment.
  • Binding constructs = 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 Readers. Functor = make a Reader that modifies the output of another Reader; Applicative = connect two Readers 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.

Hovis answered 8/1, 2013 at 1:0 Comment(1)
Great answer. You can also use the GeneralizedNewtypeDeriving extension to derive Functor, Applicative, Monad, etc. for newtypes based on their underlying types.Innutrition
S
28

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:

  • You pass the value through one of input parameters of the callable function. Drawbacks are: 1) you can't pass ALL the variables in that way - list of input parameters just blow your mind. 2) in sequence of function calls: fn1 -> fn2 -> fn3, function fn2 may not need parameter which you pass from fn1 to fn3.
  • You pass the value in scope of some monad. Drawback is: you have to get firm understanding what Monad conception is. Passing the values around is just one of great deal of applications where you may use the Monads. Actually Monad conception is incredible powerful. Don't be upset, if you didn't get insight at once. Just keep trying, and read different tutorials. The knowledge you'll get will pay off.

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.

Steerage answered 8/1, 2013 at 21:56 Comment(4)
A further drawback of using monads to implicitly pass data around is that it's very easy to find yourself writing lots of 'imperative-style' code in do-notation, which would be better off being refactored into a pure function.Beauchamp
@BenjaminHodgson Writing 'imperative-looking' code with monads in do -notation doesn't necessary mean writing side-effective (impure) code. Actually, side-effective code in Haskell may be possible inside IO monad only.Steerage
If the other function is attached to the one by a where clause, will it be accepted as a 3rd way to pass variables?Leaseholder
Good points. Your response makes evident the difference between languages with easy support for monads, like Haskell, and other functional languages which do not promote their use, and in which the first option is the norm.Indogermanic
E
3

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);
}
Edile answered 15/6, 2023 at 5:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.