Before getting to your main question about Reader
, I will start with a few remarks about applicative-versus-monad in general. While this applicative style expression...
g <$> fa <*> fb
... is indeed equivalent to this do-block...
do
x <- fa
y <- fb
return (g x y)
... switching from Applicative
to Monad
makes it possible to make decisions about which computations to perform based on results of other computations, or, in other words, to have effects that depend on previous results (see also chepner's answer):
do
x <- fa
y <- if x >= 0 then fb else fc
return (g x y)
While Monad
is more powerful than Applicative
, I suggest not thinking of it as if one were more useful than the other. Firstly, because there are applicative functors that aren't monads; secondly, because not using more power than you actually need tends to make things simpler overall. (In addition, such simplicity can sometimes bring tangible benefits, such as an easier time dealing with concurrency.)
A parenthetical note: when it comes to applicative-versus-monad, Reader
is a special case, in that the Applicative
and Monad
instances happen to be equivalent. For the function functor (that is, ((->) r)
, which is Reader r
without the newtype wrapper), we have m >>= f = flip f <*> m
. That means if take the second do-block I wrote just above (or the analogous one in chepner's answer, etc) and assume the monad being used is Reader
, we can translate it into applicative style.
Still, with Reader
ultimately being such a simple thing, why should we even bother with any of the above in this specific case? Here go a few suggestions.
To begin with, Haskellers are often wary of the bare function functor, ((->) r)
, and quite understandably so: it can easily lead to unnecessarily cryptic code when compared to "non-fancy expression[s]" in which functions are applied directly. Still, in a few select cases it can be handy to use. For a tiny example, consider these two functions from Data.Char
:
isUpper :: Char -> Bool
isDigit :: Char -> Bool
Now let's say we want to write a function that checks if a character is either an upper case letter or an ASCII digit. The straightforward thing to do is something along the lines of:
\c -> isUpper c && isDigit c
Using the applicative style, though, we can write it immediately in terms of the two functions -- or, I'm inclined to say, the two properties -- without having to note where the eventual argument goes:
(&&) <$> isUpper <*> isDigit
With an example as tiny as this one, whether to write it in this way is not a big deal, and largely up to taste -- I quite like it; others can't stand it. The point, though, is that sometimes we aren't particularly concerned about a certain value being a function, because we happen to be thinking of it as something else -- in this case, as a property -- and the fact it is ultimately a function can appear to us as a mere implementation detail.
A quite compelling example of this perspective shift involves application-wide configuration parameters: if every single function across some layer of your program takes some Config
value as an argument, chances are you will find it more comfortable treating its availability as a background assumption, rather than passing it around explicitly everywhere. It turns out that is the main use case for the reader monad.
In any case, your suspicions about the usefulness of Reader
are somewhat vindicated in at least one manner. It turns out that Reader
itself, the functions-but-wrapped-in-a-fancy-newtype functor, isn't actually used all that often in the wild. What is extremely common are monadic stacks that incorporate the functionality of Reader
, typically through the means of ReaderT
and/or the MonadReader
class. Discussing monad transformers at length would be a digression too far for the space of this answer, so I will just note that you can work with, for example, ReaderT r IO
much like you would with Reader r
, except that you can also slip in IO
computations along the way. It is not unusual to see some variant of ReaderT
over IO
as the core type of the outer layer of a Haskell application.
On a final note, you might find it interesting to see what join
from Control.Monad
does for the function functor, and then work out why that makes sense. (A solution can be found in this Q&A.)