1. Introduction
The term “monad” originally comes from category theory, a branch of mathematics that studies mathematical structures and their relations. Furthermore, monads are widely used in functional programming.
In this tutorial, we’ll take a look at what a monad is and what it means to write code using it. First, we’ll elaborate on a very simple formal definition. Then, we’ll leave the abstract world, discussing why monads are useful in functional programming and how they help in managing side effects, which are inevitable in real-world applications.
2. A Quick Formal Definition
We can see a monad as a sort of type M with three main operations:
- a type constructor, to embed a type into a monad: M T;
- a type converter, often called pure or return, to embed a value x into a monad M, that is a function T -> M T;
- a combinator, called bind and represented with the symbol >>=, that unwraps a value from the monad and applies a function to it to produce a new monadic value.
To be a monad, a type M with those three operations must meet two identity laws and an associative law. We will not formalise them here.
3. Monads in Functional Programming
Before defining what a monad is, we’ll have to clarify concepts as pure functions and side effects.
3.1. Background
In functional programming, we prioritise pure expressions, which are expressions that won’t produce any side effects. In other words, pure expressions are just as mathematical functions: we can understand what they do simply by looking at their parameters (input) and return type (output). When evaluated, they won’t change the running environment.
For example, the original values of the parameters of a function will still be the same after the function returns, and the function will not print any logs. This property is also known as referential transparency, meaning that we can always replace the expression with its result.
Referential transparency is rather common in languages like Scala or Haskell, but far less common in languages like C or C++ (which, indeed, are not functional languages). For instance, in C, it is quite normal to just input a parameter by reference and then modify its value directly.
However, side effects are needed in any program. Reading files, writing logs, querying a DB, and making HTTP calls are all side effects. Therefore, one question comes to mind: how do we model and implement side effects in pure functional languages? The answer is: monads.
3.2. Monads
A monad is a type modelling a side effect and wrapping a value. All monads have the same underlying API (remember, type constructor, type converter, and combinator). The difference lies in the side effect that the monad models. For instance, one of the most common ones is the IO monad, which takes care of interacting with the execution environment to input and output values.
Once we wrap a value inside a monad, we can access it only through the combinator. As we saw in the formal definition, the combinator unwraps the value in the monad, applies the user-provided function to the plain value, and then re-wraps it in the monad. Therefore, the combinator is a way to chain operations on the values wrapped by the monad, allowing for a clean and structured way of managing effects.
4. Monad Example
Let’s see an example of one of the simplest monads in Scala, Option: it models either the presence of a value or its absence. Let’s take a look at a simple definition:
enum MyOption[T]:
case MyNone extends MyOption[Nothing]
case MySome(x: T)
MyOption (named this way not to confuse it with Scala’s Option) is either MyNone (no value, similar to null) or MySome (wrapping a value of type T). *In this case, MyOption[T] is the type constructor, as it embeds a generic type T into the monad, MyOption[T].*
MyNone also has to wrap a type, since it’s a type constructor as well. In our case, MyNone wraps Nothing, which, in Scala, is the “bottom” type. As a matter of fact, Nothing is defined as a subtype of all types, and there’s no way to create a value of type Nothing. Therefore, we use it here to represent the absence of a value in MyOption.
Let’s add the type converter:
object MyOption:
def apply[T](x: T) = MyOption.MySome(x)
Following Scala’s naming convention, the apply method inputs a plain value of type T and returns it wrapped in a monad. In Scala, we can invoke apply methods simply by using the () operator:
println(MyOption("Baeldung"))
The example above prints MySome(Baeldung), as expected.
Lastly, let’s add the combinator:
enum MyOption[T]:
case MyNone extends MyOption[Nothing]
case MySome(x: T)
def map[U](f: T => U) = this match
case MyNone => MyNone
case MySome(x) => MyOption(f(x))
The logic of the combinator is simple: if there’s no value in the monad (MyNone), then it simply returns MyNone. Otherwise, it unwraps the underlying value, applies f to it, and then wraps the result again. Let’s see a couple of examples:
println(MyOption.MyNone map (_ => "Baeldung"))
println(MyOption(1) map (_ + 1))
In Scala, we can refer to the parameter of a function using the underscore (_). The example above prints the following two lines:
MyNone
MySome(2)
Using the map combinator, we can chain computations:
println(MyOption(10) map (_.toString) map (_ + "myString"))
The example above prints MySome(10myString), indicating that the number 10 was first turned into a String and then prepended to myString.
5. Monad Example – Python
Monads can be implemented in languages other than Scala or Haskell, although they are more common in pure functional languages.
To demonstrate this, let’s rewrite the example above in Python:
class MyOption:
def __init__(self, value):
self._value = value
def map(self, func):
if self._value is None:
return MyOption(None)
else:
return MyOption(func(self._value))
def __str__(self):
if self._value is None:
return 'None'
else:
return '{}'.format(self._value)
print(MyOption(None).map(lambda _ : "Baeldung"))
print(MyOption(10).map(lambda n: str(n)).map(lambda s: s + "myString"))
The code is functionally the same, even though Python syntax makes it look a little different. In this case, the __init__ function is equivalent to apply in Scala and lets us create a value of the MyOption monad.
To represent the absence of a value, we use None, which is the idiomatic way to define a null value in Python.
Lastly, we have to explicitly implement the __str__ function to turn objects of type MyOption into human-readable strings.
If we run the example above, we’ll see:
None
10myString
6. Monad Composition
An important property of the combinator is that it lets us change the type wrapped by the monad, but not the monad itself. In other words, we can turn a MyOption**[Int] into a MyOption[String], but the MyOption wrapper remains the same.
Over the years, there has been some research in combining monads. In this context, “combination” means the ability to chain computations using more than one monad or changing the monad type.
In practice, we can often implement a monad with some specific combination of features. However, the techniques used are typically ad hoc, and it is very difficult to find general techniques to enable composition. Normally, that requires extending the monad API we saw above and enforcing more rules on the resulting type.
7. Conclusion
Monads can be a complex concept in both functional programming and category theory.
In this article, we briefly analysed a formal definition and then dove into a more pragmatic explanation. We saw what a monad represents in programming, how we can use it to model side effects in pure functional languages, and looked at a practical example with the MyOption monad in Scala.