1

What is the intuitive meaning of join for a Monad?

The monads-as-containers analogies make sense to me, and inside these analogies join makes sense. A value is double-wrapped and we unwrap one layer. But as we all know, a monad is not a container.

How might one write sensible, understandable code using join in normal circumstances, say when in IO?

rityzmon
  • 1,945
  • 16
  • 26
  • 4
    Possible duplicate: [Monads with join instead of bind](http://stackoverflow.com/q/11234632/791604). Can you say whether this question's answers meet your needs, and if not, update your question with details about why so that we can target our answers to the parts you don't understand? – Daniel Wagner Jun 07 '16 at 00:22

5 Answers5

3

An action :: IO (IO a) is a way of producing a way of producing an a. join action, then, is a way of producing an a by running the outermost producer of action, taking the producer it produced and then running that as well, to finally get to that juicy a.

Cactus
  • 27,075
  • 9
  • 69
  • 149
3

join collapses consecutive layers of the type constructor.

A valid join must satisfy the property that, for any number of consecutive applications of the type constructor, it shouldn't matter the order in which we collapse the layers.

For example

ghci> let lolol = [[['a'],['b','c']],[['d'],['e']]]
ghci> lolol :: [[[Char]]]
ghci> lolol :: [] ([] ([] Char)) -- the type can also be expressed like this

ghci> join (fmap join lolol) -- collapse inner layers first
"abcde"
ghci> join (join lolol) -- collapse outer layers first
"abcde"

(We used fmap to "get inside" the outer monadic layer so that we could collapse the inner layers first.)

A small non container example where join is useful: for the function monad (->) a, join is equivalent to \f x -> f x x, a function of type (a -> a -> b) -> a -> b that applies two times the same argument to another function.

danidiaz
  • 26,936
  • 4
  • 45
  • 95
2

For the List monad, join is simply concat, and concatMap is join . fmap. So join implicitly appears in any list expression which uses concat or concatMap.

Suppose you were asked to find all of the numbers which are divisors of any number in an input list. If you have a divisors function:

divisors :: Int -> [Int]
divisors n = [ d | d <- [1..n], mod n d == 0 ]

you might solve the problem like this:

foo xs = concat $ (map divisors xs)

Here we are thinking of solving the problem by first mapping the divisors function over all of the input elements and then concatenating all of the resulting lists. You might even think that this is a very "functional" way of solving the problem.

Another approch would be to write a list comprehension:

bar xs = [ d | x <- xs, d <- divisors x ]

or using do-notation:

bar xs = do x <- xs
            d <- divisors
            return d

Here it might be said we're thinking a little more imperatively - first draw a number from the list xs; then draw a divisors from the divisors of the number and yield it.

It turns out, though, that foo and bar are exactly the same function.

Morever, these two approaches are exactly the same in any monad. That is, for any monad, and appropriate monadic functions f and g:

do x <- f      
   y <- g x           is the same as:    (join . fmap g) f
   return y

For instance, in the IO monad if we set f = getLine and g = readFile, we have:

do x <- getLine
   y <- readFile x    is the same as:   (join . fmap readFile) getLine
   return y

The do-block is a more imperative way of expressing the action: first read a line of input; then treat returned string as a file name, read the contents of the file and finally return the result.

The equivalent join expression seems a little unnatural in the IO-monad. However it shouldn't be as we are using it in exactly the same way as we used concatMap in the first example.

ErikR
  • 51,541
  • 9
  • 73
  • 124
1

Given an action that produces another action, run the action and then run the action that it produces.

If you imagine some kind of Parser x monad that parses an x, then Parser (Parser x) is a parser that does some parsing, and then returns another parser. So join would flatten this into a Parser x that just runs both actions and returns the final x.

Why would you even have a Parser (Parser x) in the first place? Basically, because fmap. If you have a parser, you can fmap a function that changes the result over it. But if you fmap a function that itself returns a parser, you end up with a Parser (Parser x), where you probably want to just run both actions. join implements "just run both actions".

I like the parsing example because a parser typically has a runParser function. And it's clear that a Parser Int is not an integer. It's something that can parse an integer, after you give it some input to parse from. I think a lot of people end up thinking of an IO Int as being just a normal integer but with this annoying IO bit that you can't get rid of. It isn't. It's an unexecuted I/O operation. There's no integer "inside" it; the integer doesn't exist until you actually perform the I/O.

MathematicalOrchid
  • 61,854
  • 19
  • 123
  • 220
1

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:

  1. A parse result of type a;
  2. 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.

Luis Casillas
  • 29,802
  • 7
  • 49
  • 102