I'm reading the Book of Monads and I got to the Reader
monad where the motivation for using Reader
is presented using this example:
handle :: Config -> Request -> Response
handle cfg req =
produceResponse cfg (initializeHeader cfg) (getArguments cfg req)
To avoid explicitly passing the cfg
argument the Reader
is used as following:
handle :: Request -> Reader Config Response
handle req = do header <- initializeHeader
args <- getArguments req
produceResponse header args
The part that confuses me are the functions which beside the cfg
take additional parameters. How can I achieve that?
In a pretty naive attempt I tried this:
getArguments :: Reader Config (Request -> Arguments)
produceResponse :: Reader Config (Header -> Arguments -> Response)
I also searched in a couple of books and in the Reader
docs.
CodePudding user response:
That attempt is definitely not naive, though it is not the usual way of doing things.
Since Reader r a
is really just a function r -> a
behind the scenes, your definitions of getArguments
and produceResponse
are essentially:
getArguments :: Config -> Request -> Arguments
produceResponse :: Config -> Header -> Arguments -> Response
Notice that in this case Config
is always the first parameter, so things like getArguments req
will not work - after all, Request
is the second parameter of getArguments
, not the first one, so you can't just apply getArguments
to req
.
What you need to do is to first apply getArguments
to Config
. Since we're actually working with a Reader Config ...
and not a simple function Config -> ...
, we do that by binding getArguments
:
handle req = do header <- initializeHeader
argumentGetter <- getArguments
-- rest omitted
Since getArguments
is a Reader Config (Request -> Arguments)
, argumentGetter
will simply be a function Request -> Arguments
. Using such a function is then trivial:
handle :: Request -> Reader Config Response
handle req = do header <- initializeHeader
argumentGetter <- getArguments
let args = argumentGetter req
-- rest omitted
You could then go on and apply the same treatment to produceResponse
and things would work.
However, at the start I said that this is not the usual way of doing things. Consider these definitions:
getArguments :: Request -> Reader Config Arguments
produceResponse :: Header -> Arguments -> Reader Config Response
If we unwrap the Reader
, we'll see that these are actually just:
getArguments :: Request -> Config -> Arguments
produceResponse :: Header -> Arguments -> Config -> Response
That is, Config
always comes as the last parameter. This is in contract to what we had at the start, where Config
always came first.
Defining getArguments
and produceResponse
like this works nicely in your original example:
args <- getArguments req
-getArguments
applied toreq
produces aReader Config Arguments
and binding that toargs
makesargs
anArguments
produceResponse header args
-produceResponse
applied toheader
andargs
produces aReader Config Response
, which fits nicely as the last statement in thedo
block
Note that we could do a transformation like this for Reader
since Reader
is just a function anyway, but in general for any Monad m
, there's a quite a bit of difference between m (x -> y)
and x -> m y
, with the latter being the most common for "parameterized" monadic operations.
For example, consider readFile :: FilePath -> IO String
. You provide a FilePath
and it gives you an IO action that produces the contents of the file. If instead it were IO (FilePath -> String)
, it would be an IO action that produces a function FilePath -> String
- that is, a pure function that, when given a FilePath
, produces the contents of a file. However, since it's a pure function it can't have any side effects, so it can't actually read the file and thus this would not really work (getFile
could do crazy stuff like read the whole filesystem first, but let's not get into that).
CodePudding user response:
One option I used in the past is to make a custom record type
data Context = Context
{ config :: Config
, header :: Header
, args :: Arguments
}
Then, you can use it like this:
foo :: Int -> Reader Context String
foo x = do
h <- asks header -- get the header from the implicit context
a <- asks args
doSomething x h a