I find these things easier to interpret by writing out the types and refactoring them a bit to reveal what the functions do.
Reader monad
The Reader
type is defined thus, and its join
function has the type shown:
newtype Reader r a = Reader { runReader :: r -> a }
join :: Reader r (Reader r a) -> Reader r a
Since this is a newtype
, this means that the type Reader r a
is isomorphic to r -> a
. So we can refactor the type definition to give us this type that, albeit it's not the same, it's really "the same" with scare quotes:
In the (->) r
monad, which is isomorphic to Reader r
, join
is the function:
join :: (r -> r -> a) -> r -> a
So the Reader
join is the function that takes a two-place function (r -> r -> a
) and applies to the same value at both its argument positions.
Writer monad
Since the Writer
type has this definition:
newtype Writer w a = Writer { runWriter :: (a, w) }
...then when we remove the newtype
, its join
function has a type isomorphic to:
join :: Monoid w => ((a, w), w) -> (a, w)
The Monoid
constraint needs to be there because the Monad
instance for Writer
requires it, and it lets us guess right away what the function does:
join ((a, w0), w1) = (a, w0 <> w1)
State monad
Similarly, since State
has this definition:
newtype State s a = State { runState :: s -> (a, s) }
...then its join
is like this:
join :: (s -> (s -> (a, s), s)) -> s -> (a, s)
...and you can also venture just writing it directly:
join f s0 = (a, s2)
where
(g, s1) = f s0
(a, s2) = g s1
{- Here's the "map" to the variable names in the function:
f g s2 s1 s0 s2
join :: (s -> (s -> (a, s ), s )) -> s -> (a, s )
-}
If you stare at this type a bit, you might think that it bears some resemblance to both the Reader
and Writer
's types for their join
operations. And you'd be right! The Reader
, Writer
and State
monads are all instances of a more general pattern called update monads.
List monad
join :: [[a]] -> [a]
As other people have pointed out, this is the type of the concat
function.
Parsing monads
Here comes a really neat thing to realize. Very often, "fancy" monads turn out to be combinations or variants of "basic" ones like Reader
, Writer
, State
or lists. So often what I do when confronted with a novel monad is ask: which of the basic monads does it resemble, and how?
Take for example parsing monads, which have been brought up in other answers here. A simplistic parser monad (with no support for important things like error reporting) looks like this:
newtype Parser a = Parser { runParser :: String -> [(a, String)] }
A Parser
is a function that takes a string as input, and returns a list of candidate parses, where each candidate parse is a pair of:
- A parse result of type
a
;
- The leftovers (the suffix of the input string that was not consumed in that parse).
But notice that this type looks very much like the state monad:
newtype Parser a = Parser { runParser :: String -> [(a, String)] }
newtype State s a = State { runState :: s -> (a, s) }
And this is no accident! Parser monads are nondeterministic state monads, where the state is the unconsumed portion of the input string, and parse steps generate alternatives that may be later rejected in light of further input. List monads are often called "nondeterminism" monads, so it's no surprise that a parser resembles a mix of the state and list monads.
And this intuition can be systematized by using monad transfomers. The state monad transformer is defined like this:
newtype StateT s m a = StateT { runStateT :: s -> m (a, s) }
Which means that the Parser
type from above can be written like this as well:
type Parser a = StateT String [] a
...and its Monad
instance follows mechanically from those of StateT
and []
.
The IO
monad
Imagine we could enumerate all of the possible primitive IO
actions, somewhat like this:
{-# LANGUAGE GADTs #-}
data Command a where
-- An action that writes a char to stdout
putChar :: Char -> Command ()
-- An action that reads a char from stdin
getChar :: Command Char
-- ...
Then we could think of the IO
type as this (which I've adapted from the highly-recommended Operational monad tutorial):
data IO a where
-- An `IO` action that just returns a constant value.
Return :: a -> IO a
-- An action that binds the result of a `Command` to
-- a function that computes the next step after it.
Bind :: Command x -> (x -> IO a) -> IO a
instance Monad IO where ...
Then join
action would then look like this:
join :: IO (IO a) -> IO a
-- If the action is just `Return`, then its payload already
-- is what we need to return.
join (Return ioa) = ioa
-- If the action is a `Bind`, then its "next step" function
-- `f` produces `IO (IO a)`, so we can just recursively stick
-- a `join` to its result end.
join (Bind cmd f) = Bind cmd (join . f)
So all that the join
does here is "chase down" the IO
action until it sees a result that fits the pattern Return (ma :: IO a)
, and strip out the outer Return
.
So what did I do here? Just like for parser monads, I just defined (or rather copied) a toy model of the IO
type that has the virtue of being transparent. Then I work out the behavior of join
from the toy model.