15

The Monad typeclass can be defined in terms of return and (>>=). However, if we already have a Functor instance for some type constructor f, then this definition is sort of 'more than we need' in that (>>=) and return could be used to implement fmap so we're not making use of the Functor instance we assumed.

In contrast, defining return and join seems like a more 'minimal'/less redundant way to make f a Monad. This way, the Functor constraint is essential because fmap cannot be written in terms of these operations. (Note join is not necessarily the only minimal way to go from Functor to Monad: I think (>=>) works as well.)

Similarly, Applicative can be defined in terms of pure and (<*>), but this definition again does not take advantage of the Functor constraint since these operations are enough to define fmap.

However, Applicative f can also be defined using unit :: f () and (>*<) :: f a -> f b -> f (a, b). These operations are not enough to define fmap so I would say in some sense this is a more minimal way to go from Functor to Applicative.

Is there a characterization of Monad as fmap, unit, (>*<), and some other operator which is minimal in that none of these functions can be derived from the others?

  • (>>=) does not work, since it can implement a >*< b = a >>= (\ x -> b >>= \ y -> pure (x, y)) where pure x = fmap (const x) unit.
  • Nor does join since m >>= k = join (fmap k m) so (>*<) can be implemented as above.
  • I suspect (>=>) fails similarly.
Nick Rioux
  • 972
  • 5
  • 13
  • 3
    The functor with `unit` and `>*<` is called _lax monoidal_. So your question is what is the minimal extension that would turn a lax monoidal functor into a monad. I don't know the answer, but there's one thing to keep in mind: In Haskell, lax monoidal is equivalent to applicative because Hask is closed monoidal. This wouldn't work in an arbitrary category. – Bartosz Milewski Nov 20 '20 at 06:57

1 Answers1

7

I have something, I think. It's far from elegant, but maybe it's enough to get you unstuck, at least. I started with join :: m (m a) -> ??? and asked "what could it produce that would require (<*>) to get back to m a?", which I found a fruitful line of thought that probably has more spoils.

If you introduce a new type T which can only be constructed inside the monad:

t :: m T

Then you could define a join-like operation which requires such a T:

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

The only way we can produce the T we need to get to the sweet, sweet a inside is by using t, and then we have to combine that with the result of joinT somehow. There are two basic operations that can combine two ms into one: (<*>) and joinT -- fmap is no help. joinT is not going to work, because we'll just need yet another T to use its result, so (<*>) is the only option, meaning that (<*>) can't be defined in terms of joinT.

You could roll that all up into an existential, if you prefer.

joinT :: (forall t. m t -> (m (m a) -> m (t -> a)) -> r) -> r
luqui
  • 59,485
  • 12
  • 145
  • 204