One Monadic Type Per Monadic Context

Before we can continue further, we must understand one of the implications of the bind function.

Defining the Problem

Let's look at the type signature for the bind function.

class Apply boxLike <= Bind boxLike where
  bind :: forall a b. boxLike a -> (a -> boxLike b) -> boxLike b

If we ignore the (a -> boxLike b) argument, we can summarize it like this:

If you give me a "box-like" type, I will output the same "box-like" type.

In other words, once we use bind in a given computation (e.g. do notation), we restrict all usages of bind within that same computation to the same "box-like" type we originally passed in. This restriction is a good thing. There are ways to workaround the limitation. We'll cover one workaround below, but the other workarounds will be covered in the Application Structure folder.

Throughout this work, we will refer to this restriction as the "bind outputs the same box-like type it receives" restriction.

For now, let's provide an example of this problem.

Example of the Problem

Let's say we have two Box types. They differ only in their name. Each implements the Functor, Apply, and Bind instances in the exact same way. Below, we will only show the Bind instance, but assume they have implemented the other type classes:

data Box1 a = Box1 a
data Box2 a = Box2 a

class Apply m <= Bind m where
  bind :: forall a b. m    a -> (a -> m    b) -> m    b

instance Bind Box1 where
  bind :: forall a b. Box1 a -> (a -> Box1 b) -> Box1 b
  bind               (Box1 a)   f              = f a
instance Bind Box2 where
  bind :: forall a b. Box2 a -> (a -> Box2 b) -> Box2 b
  bind               (Box2 a)   f              = f a

Recall that do notation desugars into multiple bind calls:

example :: Box1 int
example = do
  u <- Box unit
  five <- Box 5
  pure (five + 1)

-- desugars to
example =
  bind (Box unit) \u ->
    bind (Box 5) \five ->
      pure (five + 1)

The below Box1 computation compiles fine.

box1Computation :: Box1 Unit
box1Computation = Box1 unit

The below Box2 computation compiles fine:

box2Computation :: Box2 Unit
box2Computation = Box2 unit

If I write the following code, which (if any) will compile?

box1ThenBox2 :: Box2 Unit
box1ThenBox2 = do
  box1Computation
  box2Computation

box2ThenBox1 :: Box1 Unit
box2ThenBox1 = do
  box2Computation
  box1Computation

Neither will compile. In box1ThenBox2, the first computation is box1Computation. Thus, this computation should eventually output a value of the Box1 someOutput type. However, we attempt to run a computation that uses a different monad (i.e. Box2) within the Box1 monadic context. Since Box2 isn't Box1, we get a compiler error. This same error occurs when you attempt to compile box2ThenBox1.

The First Workaround: Lifting One Monad into Another

Sometimes, this restriction actually helps us write safer code. Other times, this restriction is problematic and we need to get around it.

To help develop the necessary foundation for later understanding, we'll show a general approach to workaround this restriction. We use a type class that follows this idea:

class LiftSourceIntoTargetMonad sourceMonad targetMonad where {-
  liftSourceMonad :: forall a. sourceMonad a -> targetMonad a -}
  liftSourceMonad ::           sourceMonad   ~> targetMonad

-- Note: instances of this idea might be more complicated than this one
instance LiftSourceIntoTargetMonad Box2 Box1 where {-
  liftSourceMonad :: forall a. Box2 a -> Box1 a                      -}
  liftSourceMonad ::           Box2   ~> Box1
  liftSourceMonad (Box2 a) = Box1 a

This enables something like the following. It can be pasted into the REPL and one can try it out by calling bindAttempt:

import Prelude -- for the (+) and (~>) function aliases

data Box1 a = Box1 a
data Box2 a = Box2 a

class LiftSourceIntoTargetMonad sourceMonad targetMonad where                 {-
  liftSourceMonad :: forall a. sourceMonad a -> targetMonad a                 -}
  liftSourceMonad ::           sourceMonad   ~> targetMonad

instance LiftSourceIntoTargetMonad Box2 Box1 where
  liftSourceMonad :: Box2 ~> Box1
  liftSourceMonad (Box2 a) = Box1 a

bindAttempt :: Box1 Int
bindAttempt = do
  four <- Box1 4
  six <- liftSourceMonad $ Box2 6
  pure $ four + six

-- type class instances for Monad hierarchy

instance Functor Box1 where
  map :: forall a b. (a -> b) -> Box1 a -> Box1  b
  map f (Box1 a) = Box1 (f a)

instance Apply Box1 where
  apply :: forall a b. Box1 (a -> b) -> Box1 a -> Box1  b
  apply (Box1 f) (Box1 a) = Box1 (f a)

instance Bind Box1 where
  bind :: forall a b. Box1 a -> (a -> Box1 b) -> Box1 b
  bind (Box1 a) f = f a

instance Applicative Box1 where
  pure :: forall a. a -> Box1 a
  pure a =  Box1 a

instance Monad Box1

-- Needed to print the result to the console in the REPL session

instance (Show a) => Show (Box1 a) where
  show (Box1 a) = show a