10

I got the impression that (>>=) (used by Haskell) and join (preferred by mathematicians) are "equal" since one can write one in terms of the other:

import Control.Monad (join)

join x = x >>= id
x >>= f = join (fmap f x)

Additionally every monad is a functor since bind can be used to replace fmap:

fmap f x = x >>= (return . f)

I have the following questions:

  1. Is there a (non-recursive) definition of fmap in terms of join? (fmap f x = join $ fmap (return . f) x follows from the equations above but is recursive.)

  2. Is "every monad is a functor" a conclusion when using bind (in the definition of a monad), but an assumption when using join?

  3. Is bind more "powerful" than join? And what would "more powerful" mean?

Micha Wiedenmann
  • 17,330
  • 20
  • 79
  • 123
  • For 1), I suspect that there is no such definition. All we need to do to prove it is to find a modal logic which satisfies axioms `p -> [] p` and `[][]p -> []p` (aka `return` and `join`) but fails to prove as a theorem `(p->q) -> []p -> []q` (aka `fmap`). There should be such a logic, but I'm not an expert on modal logics. Then 2) and 3) follow. – chi May 31 '19 at 08:40
  • For 3), I believe 'more powerful' means that you can define one operator/abstraction in terms of another, but not the reverse. For instance, we say that `Monad` is more powerful than `Applicative` because the `Monad` methods may be used to re-implement `Applicative` for all `Monad`s, but not the reverse. I would agree with @chi for 1) though, which would imply that 2) is correct and also that `bind` is less powerful than `join`. – bradrn May 31 '19 at 08:46
  • For 2), mathematically indeed every Monad is a functor, because a Monad is defined as a functor with extra functionality (which in Haskell terms means having `return` and `join` available and satisfying the necessary laws). That's because in category theory it doesn't make sense to look at mappings between categories that aren't functors. And as you note, it doesn't seem to be possible (although I can't claim to be 100% certain) to define `fmap` just in terms of `return` and `join`. – Robin Zigmond May 31 '19 at 09:07
  • 4
    `fmap f x = join $ fmap (return . f)` is not a recursuve definition of fmap (you cannot calculate fmap using it). It is an equation that fmap, join and return must satisfy. – n. 'pronouns' m. May 31 '19 at 11:58

2 Answers2

6

A monad can be either defined in terms of:

  • return :: a -> m a
  • bind :: m a -> (a -> m b) -> m b

or alternatively in terms of:

  • return :: a -> m a
  • fmap :: (a -> b) -> m a -> m b
  • join :: m (m a) -> m a

To your questions:

  1. No, we cannot define fmap in terms of join, since otherwise we could remove fmap from the second list above.
  2. No, "every monad is a functor" is a statement about monads in general, regardless whether you define your specific monad in terms of bind or in terms of join and fmap. It is easier to understand the statement if you see the second definition, but that's it.
  3. Yes, bind is more "powerful" than join. It is exactly as "powerful" as join and fmap combined, if you mean with "powerful" that it has the capacity to define a monad (always in combination with return).

For an intuition, see e.g. this answerbind allows you to combine or chain strategies/plans/computations (that are in a context) together. As an example, let's use the Maybe context (or Maybe monad):

λ: let plusOne x = Just (x + 1)
λ: Just 3 >>= plusOne
Just 4

fmap also let's you chain computations in a context together, but at the cost of increasing the nesting with every step.[1]

λ: fmap plusOne (Just 3)
Just (Just 4)

That's why you need join: to squash two levels of nesting into one. Remember:

join :: m (m a) -> m a

Having only the squashing step doesn't get you very far. You need also fmap to have a monad – and return, which is Just in the example above.

[1]: fmap and (>>=) don't take their two arguments in the same order, but don't let that confuse you.

mb21
  • 28,026
  • 6
  • 96
  • 118
  • Could you elaborate 1. please? It could be that `fmap` was redundant all along. – Micha Wiedenmann May 31 '19 at 11:17
  • I don't have a formal proof ready, but there is a link in my answer (or [see wikipedia](https://en.wikipedia.org/wiki/Monad_(functional_programming)#Derivation_from_functors)) to back up my statement. – mb21 May 31 '19 at 11:21
  • It's worth stressing that a monad is always an endofunctor. General functors go between categories. A monad always maps a category to itself. Haskell `Functor` is automatically an endofunctor. This is also evident in the definition of `bind`, where the morphism goes from `a` to `m b`: therefore both must be in the same category. It's worth noting that there is a third definition of a monad in terms of Kleisli arrows, Haskell `>=>`, which also requires functoriality. – Bartosz Milewski Jun 01 '19 at 02:41
5

Is there a [definition] of fmap in terms of join?

No, there isn't. That can be demonstrated by attempting to do it. Suppose we are given an arbitrary type constructor T, and functions:

returnT :: a -> T a
joinT :: T (T a) -> T a

From this data alone, we want to define:

fmapT :: (a -> b) -> T a -> T b

So let's sketch it:

fmapT :: (a -> b) -> T a -> T b
fmapT f ta = tb
    where
    tb = undefined  -- tb :: T b

We need to get a value of type T b somehow. ta :: T a on its own won't do, so we need functions that produce T b values. The only two candidates are joinT and returnT. joinT doesn't help:

fmapT :: (a -> b) -> T a -> T b
fmapT f ta = joinT ttb
    where
    ttb = undefined  -- ttb :: T (T b)

It just kicks the can down the road, as needing a T (T b) value under these circumstances is no improvement.

We might try returnT instead:

fmapT :: (a -> b) -> T a -> T b
fmapT f ta = returnT b
    where
    b = undefined  -- b :: b

Now we need a b value. The only thing that can give us one is f:

fmapT :: (a -> b) -> T a -> T b
fmapT f ta = returnT (f a)
    where
    a = undefined  -- a :: a

And now we are stuck: nothing can give us an a. We have exhausted all available possibilities, so fmapT cannot be defined in such terms.

A digression: it wouldn't suffice to cheat by using a function like this:

extractT :: T a -> a

With an extractT, we might try a = extractT ta, leading to:

fmapT :: (a -> b) -> T a -> T b
fmapT f ta = returnT (f (extractT ta))

It is not enough, however, for fmapT to have the right type: it must also follow the functor laws. In particular, fmapT id = id should hold. With this definition, fmapT id is returnT . extractT, which, in general, is not id (most functors which are instances of both Monad and Comonad serve as examples).


Is "every monad is a functor" a conclusion when using bind (in the definition of a monad), but an assumption when using join?

"Every monad is a functor" is an assumption, or, more precisely, part of the definition of monad. To pick an arbitrary illustration, here is Emily Riehl, Category Theory In Context, p. 154:

Definition 5.1.1. A monad on a category C consists of

  • an endofunctor T : C → C,

  • a unit natural transformation η : 1CT, and

  • a multiplication natural transformation μ :T2T,

so that the following diagrams commute in CC: [diagrams of the monad laws]

A monad, therefore, involves an endofunctor by definition. For a Haskell type constructor T that instantiates Monad, the object mapping of that endofunctor is T itself, and the morphism mapping is its fmap. That T will be a Functor instance, and therefore will have an fmap, is, in contemporary Haskell, guaranteed by Applicative (and, by extension, Functor) being a superclass of Monad.

Is that the whole story, though? As far as Haskell is concerned. we know that liftM exists, and also that in a not-so-distant past Functor was not a superclass of Monad. Are those two facts mere Haskellisms? Not quite. In the classic paper Notions of computation and monads, Eugenio Moggi unearths the following definition (p. 3):

Definition 1.2 ([Man76]) A Kleisli triple over a category C is a triple (T, η, _*), where T : Obj(C) → Obj(C), ηA : A → T A for A ∈ Obj(C), f* : T A → T B for f : A → T B and the following equations hold:

  • ηA* = idT A
  • ηA; f* = f   for   f : A → T B
  • f*; g* = (f; g*)*   for   f : A → T B   and   g : B → T C

The important detail here is that T is presented as merely an object mapping in the category C, and not as an endofunctor in C. Working in the Hask category, that amounts to taking a type constructor T without presupposing it is a Functor instance. In code, we might write that as:

class KleisliTriple t where
    return :: a -> t a
    (=<<) :: (a -> t b) -> t a -> t b

-- (return =<<) = id
-- (f =<<) . return = f
-- (g =<<) . (f =<<) = ((g =<<) . f =<<)

Flipped bind aside, that is the pre-AMP definition of Monad in Haskell. Unsurprisingly, Moggi's paper doesn't take long to show that "there is a one-to-one correspondence between Kleisli triples and monads" (p. 5), establishing along the way that T can be extended to an endofunctor (in Haskell, that step amounts to defining the morphism mapping liftM f m = return . f =<< m, and then showing it follows the functor laws).

All in all, if you write lawful definitions of return and (>>=) without presupposing fmap, you indeed get a lawful implementation of Functor as a consequence. "There is a one-to-one correspondence between Kleisli triples and monads" is a consequence of the definition of Kleisli triple, while "a monad involves an endofunctor" is part of the definition of monad. It is tempting to consider whether it would be more accurate to describe what Haskellers did when writing Monad instances as "setting up a Klesili triple" rather than "setting up a monad", but I will refrain out of fear of getting mired down terminological pedantry -- and in any case, now that Functor is a superclass of Monad there is no practical reason to worry about that.


Is bind more "powerful" than join? And what would "more powerful" mean?

Trick question!

Taken at face value, the answer would be yes, to the extent that, together with return, (>>=) makes it possible to implement fmap (via liftM, as noted above), while join doesn't. However, I don't feel it is worthwhile to insist on this distinction. Why so? Because of the monad laws. Just like it doesn't make sense to talk about a lawful (>>=) without presupposing return, it doesn't make sense to talk about a lawful join without pressuposing return and fmap.

One might get the impression that I am giving too much weight to the laws by using them to tie Monad and Functor in this way. It is true that there are cases of laws that involve two classes, and that only apply to types which instantiate them both. Foldable provides a good example of that: we can find the following law in the Traversable documentation:

The superclass instances should satisfy the following: [...]

In the Foldable instance, foldMap should be equivalent to traversal with a constant applicative functor (foldMapDefault).

That this specific law doesn't always apply is not a problem, because we don't need it to characterise what Foldable is (alternatives include "a Foldable is a container from which we can extract some sequence of elements", and "a Foldable is a container that can be converted to the free monoid on its element type"). With the monad laws, though, it isn't like that: the meaning of the class is inextricably bound to all three of the monad laws.

Community
  • 1
  • 1
duplode
  • 31,361
  • 7
  • 69
  • 130