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