Sunday, August 25, 2024 | 9 min read

Lifting Functions into a Monadic Context

This article is based on my studies of Monads in Haskell. I will describe what I’ve learned using references and a simple theory aimed at practical applications in Haskell. Monads are rooted in category theory, as they involve concepts such as functors, morphisms, endofunctors, and more.

I’m not an expert in monads or category theory — just an enthusiast — so feel free to submit a pull request or create an issue if you find any errors, so they can be corrected.

Background

“Monad is a way of composing embellished functions. It’s not about side effects or state. It’s about composition. [...] embellished functions may be used to express a variety of effects or state1.”

“Monads were first introduced to computer science by Moggi2 and later expanded by Wadler3 to express the semantics of programs with computational effects. Type and effect inference were also introduced to account for these effects4.”

“It is well known that purely functional programs are easier to reason about than imperative ones, and one of the first pieces of advice in Kernighan and Pike's The Practice of Programming is 'Be careful with side effects'4.”

“There are many equivalent ways of defining a monad1”, “a monad introduces a type operator (μ)(\mu), where (μτ)(\mu\tau) represents computations returning values of type (τ)(\tau). The type (μτ)(\mu\tau) can be interpreted as an abstract datatype that encapsulates computations returning values of type (τ)(\tau). These computations are accessed and manipulated using two fundamental operations: unit and bind4.”

Where (μ)(\mu) is a monadic operator and (τ)(\tau) is a generic type. So, (μτ)(\mu\tau) indicates a type that represents a computation that eventually returns a value of type (τ)(\tau). For example, in Haskell, Maybe Int would be a monad that can compute and return an Int value or nothing.

Unit and Bind

Unit and Bind are fundamental functions of monads. In monadic terms, the unit function (often called return in haskell) is a fundamental building block of a monad and it is expressed as (τμτ)(\tau \rightarrow \mu \tau). It lifts a value of type (τ)(\tau) into the monadic context (m τ)(m\ \tau), transforming it into a computation of type (μτ)(\mu\tau)4. In Haskell, it looks like:

return :: a -> m a

This corresponds to the mathematical expression (η:τμτ)(\eta: \tau \rightarrow \mu \tau). Where (μ)(\mu) represents the monadic type constructor m and (η)(\eta) is a natural transformation.

“Bind, sometimes referred to as star, can be represented as (μτ(τμτ)μτ)(\mu \tau \rightarrow (\tau \rightarrow \mu \tau') \rightarrow \mu \tau'). Bind takes a computation of type (μτ)(\mu\tau) (a computation that eventually returns a value of type (τ)(\tau)) and a function that transforms the value (τ)(\tau) into a new computation of type (μτ)(\mu \tau'). Bind then chains these two computations, passing the result of the first computation to the function that produces the second computation4.”

“In Haskell, bind is represented by >>=. The >>= function takes the value from a monad container and passes it to a function to produce a monad container containing a new value, possibly of a different type. The >>= function is known as "bind" because it binds the value in a monad container to the first argument of a function. By adding logic to the binding function, a monad can implement a specific strategy for combining computations in the monad5.”

For example, given a Maybe Int and a function f :: Int -> Maybe String, bind allows us to chain these computations to produce a new computation of type Maybe String.

  • μτ\mu \tau == represents a monadic value, such as Maybe Int.
  • τμτ\tau \rightarrow \mu \tau' == is a function that takes an Int and returns a Maybe String.
  • μτ\mu \tau' == is the resulting monadic value, Maybe String.

Laws

The laws that define the properties of monads are grounded in category theory because they delve into the realm of categories, endofunctors, and functors. This is a more intricate area, but briefly:

Left Identity (μTη=id)( \mu \circ T\eta = id ):

When you "lift" a value into the monad's context (using (η)(\eta)) and then "flatten" it back down (using (μ)(\mu)), the final result is just the original value, unchanged.

  • (μ)(\mu): This "flattens" a nested structure (for example, T (T a) becomes T a).
  • (Tη)(T\eta): Here, (T)(T) is the functor associated with the monad, and (η)(\eta) is the unit (also called return or pure in some contexts). (Tη)(T\eta) indicates that we first apply the functor (T)(T) to the result of applying the natural transformation (η)(\eta). In other words, (η)(\eta) encapsulates a value into a computation (for example, it turns a into T a), and then (T)(T) is applied to transform it further.
  • (μTη)(\mu \circ T\eta): The composition operator ()(\circ) denotes function composition. This means we first apply (Tη)(T\eta) and then apply (μ)(\mu) to the result. Concretely, think of it as first applying return and then "flattening" the result.
  • (=id)(= id): This equation means that the result of this composition is the same as applying the identity function (id), i.e., the original value with no modifications.
return a >>= f  =  f a

Right Identity (μηT=id)( \mu \circ \eta T = id ):

The idea is that if you encapsulate a value inside the monad's context and then flatten it (using (μ)(\mu)), the result should be the original value, unchanged. This is a second way to ensure that monads preserve identity.

  • (ηT)(\eta T): Here, (ηT)(\eta T) means that we first apply the functor (T)(T) to the value and then apply the unit (η)(\eta). In other words, we're encapsulating the value into a computation and then applying the functor to that computation.
  • (μηT)(\mu \circ \eta T): This means we first encapsulate the value using (η)(\eta), apply the functor (T)(T), and then "flatten" it with (μ)(\mu).
  • (=id)(= id): Again, identity means that the final result should be the original value with no modifications.
m >>= return  =  m

Associativity (μTμ=μμT)( \mu \circ T\mu = \mu \circ \mu T ):

This law ensures that the order in which you apply the flattening operations doesn't matter. You can flatten part of the structure first and then the other part, or flatten everything all at once. The final result will be the same.

  • (Tμ)(T\mu): Here, we apply the functor (T)(T) to a computation that has already been flattened once with (μ)(\mu). This results in a nested computation.
  • (μTμ)(\mu \circ T\mu): First, we apply (Tμ)(T\mu)—i.e., use the functor to transform the computation—and then apply (μ)(\mu) to flatten it again. This should be equivalent to...
  • (μμT)(\mu \circ \mu T): Here, we apply (μ)(\mu) directly, then apply (T)(T) to the resulting computation, before flattening it a second time.
(m >>= f) >>= g  =  m >>= (\x -> f x >>= g)

Haskell Monads

The Monad typeclass includes three essential operations, but for a complete minimal Monad instance, you only need to define the >>= function6.

(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
return :: a -> m a

The ">>" operator

“The >> operator sequences two actions while discarding any resulting value of the first action6”. For example, here the >> operator ignores the result of the first action (Just 5), and returns the result of the second action (Just "Second Action").

-- The type signature is:
-- (>>) :: Maybe Int -> Maybe String -> Maybe String
ignm :: Maybe String
ignm = Just 5 >> Just "Second Action"

The ">>=" operator

“The >>= operator is called bind and is — or, at least, comprises — what makes Monad special. Typically, when working with monads, we apply the bind function (>>=). We might use it explicitly, or indirectly through the do notation6”.

The bind operator (>>=) is responsible for "chaining" monadic operations, passing the unwrapped value (for example, the value inside Just in the case of Maybe) to the next function, and so forth.

-- (>>=) :: m a -> (a -> m b) -> m b
 
-- Just x     :: Maybe Int            -- m a
-- \a -> ...  :: Int -> Maybe Int     -- a -> m b
-- (>>=)      :: Maybe Int -> (Int -> Maybe Int) -> Maybe Int -- m b
bind :: Int -> Maybe Int
bind x = Just x >>= \a -> Just (a + 1)

The "return" operator

The monadic context in Maybe is the encapsulation of the value in the Just or Nothing constructor. This encapsulation allows the value to be manipulated within a monadic "sequence of operations" without leaving the Maybe context. Therefore, return (x * 2) places the value of x * 2 inside the monadic Maybe context, which is Just.

-- :: Int -> Maybe Int -- Puts the value in the Maybe context (Just)
-- return (x * 2) == Just (x * 2)
justr :: Int -> Maybe Int
justr x = return (x * 2)

Summary

  • Monad: It's a context that encapsulates a value (or the absence of it) and handles side effects in a controlled way.
  • bind: Unwraps a monad and flattens the result, passing it to another function.
  • unit: Puts a value into a monadic context.

There is much more to explore regarding Monads, including a deeper analysis of categories, functors, and Applicative in Haskell, but I will reserve those topics for a future post to maintain the brevity and clarity of this one. Thank you for reading, and should you have any questions or contributions, please submit an issue.

Footnotes

  1. M. Bartosz, "Monads: Programmer’s Definition" 2

  2. E. Moggi, “Notions of computation and monads”. Information and Computation, vol. 93, no. 1, pp. 55–92, Jul. 1991, doi: 10.1016/0890-5401(91)90052-4.

  3. P. Wadler, “Monads for functional programming,” pp. 233–264, 1993, doi: 10.1007/978-3-662-02880-3_8.

  4. J. C. FILLIATRE, “A theory of Monads Parametrized by Effects”. Sept. 2003. . 2 3 4 5

  5. Haskell Wiki, "All About Monads"

  6. Chris Allen, Julie Moronuki, Haskell Programming from First Principles. 2 3