Home > OS >  Boilerplate code in the definition of a monad
Boilerplate code in the definition of a monad

Time:12-10

Since the Functor-Applicative-Monad Proposal, Monads are a subclass of Applicative which in turn is a subclass of Functor. Mathematically speaking, this seems to be a sensible choice and I do not have any problems with that.

However, what is irritating me is that one is required to write down the Functor and Applicative instances even if the corresponding laws for fmap and pure and <*> are fixed by the monad laws anyways. Indeed, in the above linked proposal, they write themselves: "You can simply derive these instances from the Monad by adding the following code":

import Control.Applicative (Applicative(..))
import Control.Monad       (liftM, ap)

instance Functor m where
    fmap = liftM

instance Applicative m where
    pure  = {- move the definition of `return` from the `Monad` instance here -}
    (<*>) = ap

and in most tutorials, you will simply see the equality pure=return and people then go on to define return in the Monad instance as before. Therefore, effectively, 10 lines of boilerplate code were added for everyone who simply wants to define a Monad instance.

In mathematics, one usually also does not operate in this way. For example, when defining the composition laws for a certain ring, the reader is usually not explicitly reminded again that rings are special instances of abelian groups under addition which in turn are special instances of groups. This is clear by definition anyways.

Thus my question is: Even though it is fine that Monads are a subclass of Applicative which in turn is a subclass of Functor, couldn't this just "happen in the background" in the sense that the compiler inserts the boilerplate code instead of the programmer?

CodePudding user response:

The advice from the documentation is for legacy types that used to have a Monad instance defined but no Applicative one. Then defining those instances in terms of the existing Monad one is a quick and reliable fix to get the package to compile again.

However, this is not how it should go for new code, since Monad is actually the more advanced class. The recommended way is to

  1. Derive the Functor instance. There is always only one possible way this can be done, and the compiler can do it for you.

    {-# LANGUAGE DeriveFunctor #-}
    data YourMonadType a = ...
      deriving (Functor)
    
  2. Define Applicative. This is not quite automatic, and sometimes <*> can be a bit counterintuitive, but you'll get the hang of it and a direct implementation can actually be more efficient than (<*>) = ap.

    instance Applicative YourMonadType where
      pure = {- direct definition here -}
      q <*> r = {- direct definition here -}
    
  3. Define Monad. This won't require return at all (because it's default = pure), just >>=.

    instance Monad YourMonadType where
      q >>= f = {- direct definition here -}
    

    Side note: arguably the method of Monad should actually be join instead of >>=. Whilst return and >>= give rise to all the Functor and Applicative methods, join is an “orthogonal feature” to them.


In modern GHC it is also possible to obtain a derive-instance for Applicative by using the via strategy on the WrappedMonad newtype:

{-# LANGUAGE DerivingVia, DeriveFunctor #-}

import Control.Applicative

data YourMonadType a = YourMonadType a a
  deriving stock (Functor, Show)
  deriving Applicative via (WrappedMonad YourMonadType)

instance Monad YourMonadType where
  return x = YourMonadType x x
  YourMonadType x y >>= f = YourMonadType fx fy
   where YourMonadType fx _ = f x
         YourMonadType _ fy = f y
ghci> YourMonadType ( 1) (*2) <*> YourMonadType 10 20
YourMonadType 11 40
  • Related