11

In Haskell, monads are defined in terms of the functions return and bind, where return has type a -> m a and bind has type m a -> (a -> m b) -> m b. It's been pointed out before that monads can also be defined in terms of return and join, where join is a function with type m (m a) -> m a. Bind can be defined in terms of join, but is the reverse possible? Can join be defined in terms of bind?

Without join, I have no idea what I'd do if I ever somehow got ahold of a "twice wrapped" monadic value, m (m a) - none of the functor or monad operations "remove any layers", so to speak. If this isn't possible, why do Haskell and many other monad implementations define them in terms of bind? It seems strictly less useful than a join-based definition.

Community
  • 1
  • 1
Jack
  • 2,083
  • 11
  • 23
  • 5
    Note "also": if join couldn't be defined in terms of bind, it would _have_ to be a member of `Monad` (or wouldn't be defined for all monads), and it isn't. – Alexey Romanov Dec 21 '15 at 15:00

5 Answers5

13

It is possible:

join :: Monad m => m (m a) -> m a
join m = (m >>= id)

Note the tricky instantiation of >>=:

(>>=) :: m b -> (b -> m c) -> m c
-- choosing b ~ m a , c ~ a
(>>=) :: m (m a) -> (m a -> m a) -> m a

so we can correctly choose id for the second argument.

chi
  • 101,733
  • 3
  • 114
  • 189
6

Yes it's fairly simple:

join m = m >>= id
Lee
  • 133,981
  • 18
  • 209
  • 268
6

Although the question has already been sufficiently answered, I thought it was worth noting the following for any Haskell newcomers.

One of the first things you see when learning about monads in Haskell is the definition for lists:

instance Monad [] where
    return x  =  [x]
    xs >>= f  =  concatMap f xs

Here, we see that the functionality of bind for lists is equivalent to concatMap, just with the arguments flipped around. This makes sense when inspecting the types:

concatMap ::            (a ->  [b]) ->  [a] ->  [b]
bind      :: Monad m => (a -> m b ) -> m a  -> m b        -- (>>=) flips the arguments

It also makes intuitive sense that the definition of join for lists is equivalent to
concat :: [[a]] -> [a].

The names of the functions may make it a little obvious, but concat can be recovered from concatMap by keeping the internal lists as they are, in order to cancel out the "map" part of concatMap:

concatMap       id xs      
  = concat (map id xs)
  = concat (    id xs)
  = concat         xs      -- or simply 'concat = concatMap id'

The same property holds true for monads in general:

join x  =  x >>= id        -- or 'join = bind id'

This really comes from the fact that

bind f m  =  join (fmap f m)

so that

bind id m  =  join (fmap id m)           -- bind id  =  join . fmap id
           =  join (     id m)           --          =  join .      id
           =  join          m            --          =  join

because all monads are Functors first, and by Functor laws fmap id === id.

Will Ness
  • 62,652
  • 8
  • 86
  • 167
N. Prindle
  • 71
  • 2
  • 2
4

Bind (>>=) does in fact "remove a layer":

(>>=) :: Monad m => m a -> (a -> m b) -> m b

Intuitively it "gets some as out of the m a", and feeds then to the a -> m b function, and then produces a single m b from the results.

People usually say that it requires the function argument to wrap up its output again in m, but that's not actually the case. It requires the function's output to be something wrapped up in m, but it doesn't matter where the wrapping came from.

In the case of implementing join we're starting from something "double-wrapped": m (m a). We can plug that into the signature for bind and immediately figure out the type of function we could use when binding a "double-wrapped" value:

m (m a) -> (m a -> m b) -> m b

Now the function used with bind here is going to receive a value that's already wrapped in m. So we don't have to "re-wrap" anything; if we return it unmodified it'll already be the right type for the output. Effectively that's "removed one layer of wrapping" - and this works for any layer but the last one.

So that tells us we just have to bind with id:

join = (>>= id)
Ben
  • 56,956
  • 19
  • 113
  • 151
0

Can join be defined in terms of bind?

TL;DR answer: Yes.

join ∷ (Monad m) ⇒ m (m a) → m a
join = (=<<) id

The longer answer: To add some subtleties that have yet to be mentioned I'll provide a new answer, starting by expanding upon Lee's answer, because it is worth noting that their answer can be simplified. Starting with the original:

join ∷ (Monad m) ⇒ m (m a) → m a
join m = m >>= id

One can look for an Eta conversion (η-conversion) opportunity to make the function definition point-free. To do this we want to first rewrite our function definition without the infix >>= (as would likely be done if we were calling >>= by the name bind in the first place).

join m = (>>=) m id

Now observe that if we use the flip function, recalling:

-- defined in Data.Function
-- for a function of two arguments, swap their order
flip ∷ (a → b → c) → b → a → c
flip f b a = f a b

One may now use flip to put the m in position for an η-reduction:

join m = (flip (>>=)) id m

Applying the η-reduction:

join = (flip (>>=)) id

Noticing now that flip (>>=) can be replaced with (=<<) (defined in Control.Monad):

join = (=<<) id

Finally we can see shorter, point-free definition:

join ∷ (Monad m) ⇒ m (m a) → m a
join = (=<<) id

Where (=<<) has type:

(=<<) ∷ ∀ (m ∷ * -> *) a b. (Monad m) ⇒ (a → m b) → m a → m b

which in the process gets instantiated to:

(=<<) ∷ (m a → m a) → m (m a) → m a

Additionally, one may also notice that if we put the code above back into infix form, the flip becomes implicit, and we get the same final answer as Ben does:

join = (>>= id)
subttle
  • 61
  • 6
  • How is that a simplification? Point-free is not always simpler or better. – dfeuer Mar 23 '21 at 04:35
  • I definitely agree that point-free is most certainly not *necessarily* "simpler" nor "better", but in this case I would argue that the η-reduction makes the final answer simpler. For example, from the link in my answer, one could argue: `sum = foldr (+) 0` is "simpler" and "better" than, `sum' xs = foldr (+) 0 xs` Because `sum` emphasizes the essence of what is occurring in the function's definition by omitting the superfluous. I realize "simpler" could be interpreted subjectively, so I can see why you'd ask but I feel justified in my answer. – subttle Mar 24 '21 at 07:18
  • Additionally (I ran out of character space), a lot of people learn to use monads in Haskell by starting with the `concatMap` example for lists (as was even done in one of the answers here), and it so happens that `concatMap` has it's arguments in the same order as `(=< – subttle Mar 24 '21 at 07:38