gostriderful
gostriderful

Reputation: 99

What is the purpose of local & asks functions in Reader monad

While I try to familiar the usage for Reader monad in Haskell, I noticed there are two similar functions provided in the mtl library.

asks :: MonadReader r m => (r -> a) -> m a

local :: MonadReader r m => (r -> r) -> m a -> m a

For example,

import Control.Monad.Reader

changeEnv :: String -> String
changeEnv = ("Prefix " ++)

getLength :: Reader String Int
getLength = do
  e <- ask
  return $ length e

runReader (local changeEnv getLength) "123" 
10

runReader (asks (length . changeEnv)) "123"
10

runReader (asks (length . local changeEnv id)) "123"
10

It seems we can compute the same result using either asks or local.

Is there any result that cannot compute from either one of them?

For now, I can only say using local allows me to separate the function (changeEnv) changing the environment and a function (getLength) that returns the result of the Reader. However, the same could have achieve using asks as well.

Is there any special reason that we need both of them?

Upvotes: 1

Views: 605

Answers (2)

chi
chi

Reputation: 116139

In your case, you were able to replace local changeEnv getLength with an equivalent expression involving asks. However, to do so, you had to inspect the code of getLength and manually insert changeEnv every time the code used ask.

The point of local is to allow that transformation without having to rewrite all the code of getLength. Imagine an extreme scenario where instead of getLength we have a a very complex action, possibly also invoking other actions, so that in total we touch a few thousand lines of code.

Then we find a local f action we would like to replace. In such case, we would need to rewrite all of the code of action and its dependencies, just to insert f in all the right places. This is long, and also inefficient since in this way we recompute f many times.

local avoids that. Of course, strictly speaking, we do not have to use local, since we could break the abstraction by pattern matching on the Reader constructor (or abuse runReader) so that we can pre-compose f at the right spot. In doing so, however, we would have reimplemented local.

Further, note that local works not only on the simple Reader a monad, but on all monads of class MonadReader, where the actions could very well be complex ones.

Upvotes: 0

David Fletcher
David Fletcher

Reputation: 2818

We have asks f = local f ask for any f, so it's true that you could always write asks in terms of local. Though normally you would think of asks f as a shorthand for fmap f ask.

In the other direction however, consider something like

local changeEnv (do x <- getLength
                    y <- getReverse
                    pure (replicate x y))

How do you do this with asks in general? You'd have to include the entire body and put the environment into it explicitly:

asks ((\s -> replicate (length s) (reverse s)) . changeEnv)

You can always do this if you want because you can always translate any Reader computation into ordinary functions. But then you might as well not use Reader at all.

This doesn't mean local is necessary either - it's just a convenience for nesting readers, and you can always use runReader directly instead like:

do s' <- asks changeEnv
   let x = runReader (do ...) s'
   ...

and I suppose you can then cram it all inside asks if you really want to, like

asks (runReader (do ...) . changeEnv)

but using local seems nicer.

Upvotes: 1

Related Questions