The main purpose of monads is to ease the burden of working with computational contexts.
Take parsing for example. In parsing we attempt to turn strings into data. And the Parser context is turning strings into data.
In parsing, we might attempt to read a string as an integer. We can probably succeed if the string is "123", but may fail for the string "3,6". So failure is part of the context of parsing. Another part of parsing is handling the current location of the string that we are parsing, and that is also included in the "context". So if we want to parse an integer, then a comma, then another integer our monad helps us parse the above "3,6" with something like:
intCommaInt = do
i <- intParse
commaParse
j <- intParse
return (i,j)
The definition of the parser monad would need to handle some internal state of a string that is being parsed so that the first intParse will consume the "3", and pass the rest of the string, ",6" to the rest of the parser. The monad helps by allowing the user to ignore passing the unparsed string around.
To appreciate this, imagine writing some functions which manually passes the parsed string around.
commaParseNaive :: String -> Maybe (String,())
commaParseNaive (',':str) = Just (str,())
commaParseNaive _ = Nothing
intParseNaive :: String -> Maybe (String,Int)
intParseNaive str = ...
Note: I left intParseNaive
unimplemented, because its more complex and
you can guess what it's supposed to do. I made the comma parse return a boring (), so that both functions have a similar interface, alluding to how they might both be the same type of monadic thing.
Now to compose the two naive parsers above we connect the output of the previous parse to the input of the subsequent parse--if the parse succeeded. But we do that every single time we want to parse one thing then another. The monad instance lets the user forget about that noise and just concentrate on the next part of the current string.
There are a lot of common programming situations where the particulars of what the program does can be modeled by a monadic context. Its a general concept. Knowing something is a monad lets you know how to combine monadic functions, i.e. inside a do
block. But you still need to know what the specifics of the context are, as Roman stresses in his answer.
The monad interface has two methods, return
and (>>=)
. These determine the context. I like to think in terms of the do
notation, and so I paraphrase a few more examples below, in terms of putting a pure value into context, return
, and taking it out of context within a do block, a <- (monadicExpression :: m a)
Maybe
: computations with failure.
return a
: A computation which reliably, always yields a
a <- m
: m
was run and succeeded.
Reader r
: Computations which may use some "global" data r
.
return a
: A computation which doesn't need the global.
a <- m
: m
was run, possibly using the global, and yielded a
State s
: Computations with an internal state, like a read/write mutable variable that is available to them.
return a
: A computation which leaves that state unchanged.
a <- m
: m
was run, possibly using/modifying the state, and yielded a
IO
: Computations which may do some input/output interaction in the real world.
return a
: An IO computation which will not actually do IO.
a <- m
: m
was run, possibly through interaction with a file, user, network, etc, and yielded an a
.
The above listed, along with parsing, will get you a long way to using any monad effectively. I am leaving out some things as well. First, the a <- m
isn't the whole story of the bind (>>=). For instance for my maybe explanation doesn't explain what to do on a failed computation--abort the rest of the chain. Secondly I also ignore the monad laws, which I can't explain anyway. But their purpose is mainly to ensure that return
is like doing nothing to the context, e.g. IO return doesn't send missles, State return doesn't touch state, etc.
Edit. Since I can't nicely inline the answer to the comment, I'll address it here. commaParse
is a notional example for a fictional parser combinator, of the type commaParse :: MyUndefinedMonadicParserType ()
. I could implement this parser by, for example
import Text.Read
commaParse :: ReadPrec ()
commaParse = do
',' <- get
return ()
where get :: ReadPrec Char
is defined in Text.ParserCombinators.ReadPrec
and takes the next character from the string being parsed. I utilize the fact that ReadPrec
has a MonadFail
instance and use the monadic bind as a pattern match against ','
. If the bound character was not a comma then the next character in the parsed string was not a comma and the parse fails.
The next part of the question is important, as it stresses the subtle magic of a monadic parser, "where does it get his input from?" The input is part of what I've been referring to as the monadic context. In a sense, the parser just knows that it will be there and the library provides primitives to access it.
To elaborate: writing the original intCommaInt = do
block my thinking is something like, "At this point in the parse I expect an integer (An string with a valid integer representation), I'll call that 'i'. Next there is a comma (which returns a ()
, no need to bind that to a variable). Next should another integer. Ok, parse successful, return the two integers. Notice, I don't need to think things like. "Grab the current string that i'm parsing, pass the remaining string on." That boring stuff is handled by the definition of the parser. My knowledge of the context is that the parser will be working on the next part of the string, whatever that is.
But of course, the string will need to be provided eventually. One way to do this, is the standard "running" a monad pattern:
x = runMonad monadicComputation inputData
in our case, something along the lines of
case readPrec_to_S intCommaInt 0 inputString of
[] -> --failed parse value
((i,j),remainingString):moreParses -> --something using i,j etc.
The above is a standard pattern wherein the monad represents some type of computer that needs input. However, for ReadPrec
in particular, the running is done through the standard Read
type class and just calling read "a string to parse"
.
So, if we were to make (Int,Int)
a member of Read
with
class Read (Int,Int) where
readPrec = intCommaInt
then we could call things like the following, which would all used the underlying Read
instance.
read "1,1" :: (Int,Int) --Success, now can work with int pairs.
read "a,b" :: (Int,Int) --Fails at runtime
readMaybe "a,b" :: Maybe (Int,Int) -- Returns (Just (1,1))
readMaybe "1,1" :: Maybe (Int,Int) -- Returns Nothing
However, the read Class already has an implementation for (Int,Int), so we cant write that class directly. Instead we would might define a new type,
newtype IntCommaInt = IntCommaInt (Int,Int)
and define our parser/ReadPrec in terms of it.