Lesson 5

Monads can make your code much cleaner and safer, especially for handling errors and chaining operations. Today, we aim to understand what a *monad* is, specifically the `Maybe`

monad, and see how it helps in functional programming. By the end of this lesson, you'll know how to create and use a `Maybe`

monad and how to chain operations using the `bind`

method.

A *monad* is a design pattern used in functional programming to handle program logic that involves wrapping a value, performing operations, and managing side effects. Monad is an extension of Functor. Effectively, Monad does the same thing as the Functor, but it provides additional logic to handle all possible scenarios like data of incorrect type or `None`

instead of value.

The `Maybe`

monad represents values that might or might not exist. It helps avoid errors when you try to use missing values. Let's create the `Maybe`

monad in Python step-by-step:

Python`1from typing import Optional, TypeVar, Generic 2 3T = TypeVar('T') 4 5class Maybe(Generic[T]): 6 def __init__(self, value: Optional[T] = None): 7 self._value = value`

This constructor holds a value that can either be `None`

(absence of value) or any other type `T`

.

To work with our `Maybe`

monad, we check if it contains a value or not. We do this using the `is_just`

method.

Python`1from typing import Optional, TypeVar, Generic 2 3T = TypeVar('T') 4 5class Maybe(Generic[T]): 6 def __init__(self, value: Optional[T] = None): 7 self._value = value 8 9 def is_just(self) -> bool: 10 return not self.is_nothing()`

** is_just** checks if the

`Maybe`

monad contains a value.Next, we implement the `map`

method, similar to the one we had in functors.

Python`1from typing import Callable 2from typing import Optional, TypeVar, Generic 3 4T = TypeVar('T') 5U = TypeVar('U') 6 7 8class Maybe(Generic[T]): 9 def __init__(self, value: Optional[T] = None): 10 self._value = value 11 12 def is_just(self) -> bool: 13 return self._value is not None 14 15 def map(self, f: Callable[[T], U]) -> "Maybe[U]": 16 if self.is_just(): 17 return Maybe(f(self._value)) 18 return Maybe()`

It works simply. First, we check if our monad has a value. If it does, we apply the given function to it and wrap the result back into monad. Otherwise, we return a new empty monad. So far, monad works in the same manner as a functor, but there is a problem with this approach. Let's explore it.

Consider a function that returns a Maybe itself:

Python`1def safe_divide(a: float, b: float) -> Maybe[float]: 2 if b == 0: 3 return Maybe(None) 4 return Maybe(a / b)`

This division function handles division by zero by returning an instance of `Maybe`

class. If `b`

is zero, the result will be a `None`

, safely stored inside the monad.

Consider the following scenario: we use `Maybe`

to divide `10`

by `2`

with `safe_divide`

:

Python`1if __name__ == "__main__": 2 safe_divide_2 = partial(safe_divide, b=2) 3 value = Maybe(10) 4 result = value.map(safe_divide_2)`

Note that `safe_divide`

returns an instance of `Maybe`

, and the `map`

method of `Maybe`

wraps it's result in `Maybe`

. A lot of maybes here, right? It is! In this case, the `result`

variable will have the type `Maybe[Maybe[int]]`

, which is a nested `Maybe`

. It is a problem, as it doesn't allow us to work with `result`

further in the same manner as with the original `value`

.

To solve it, let's add a new method to our monad called `join`

. It will unwrap the monad's value, returning it to its normal state.

Python`1from typing import Callable 2from typing import Optional, TypeVar, Generic 3 4T = TypeVar('T') 5U = TypeVar('U') 6 7 8class Maybe(Generic[T]): 9 def __init__(self, value: Optional[T] = None): 10 self._value = value 11 12 def is_just(self) -> bool: 13 return self._value is not None 14 15 def map(self, f: Callable[[T], U]) -> "Maybe[U]": 16 if self.is_just(): 17 return Maybe(f(self._value)) 18 return Maybe() 19 20 def join(self) -> "Maybe[T]": 21 if self.is_just() and isinstance(self._value, Maybe): 22 self._value = self._value._value 23 return self`

The implemented `join`

method works straightforward:

- It checks if the monad holds a value.
- It checks if the value is also a
`Maybe`

instance, meaning we have a situation of nested monads. - If both conditions are true, it unwraps the monad by assigning
`self._value`

to the inner value. - If conditions are false, meaning there is no nested
`Maybe`

, it simply does nothing.

Finally, we combine `map`

and `join`

into a single `bind`

method to make it easy to use and clear:

Python`1from functools import partial 2from typing import Callable 3from typing import Optional, TypeVar, Generic 4 5T = TypeVar('T') 6U = TypeVar('U') 7 8 9class Maybe(Generic[T]): 10 def __init__(self, value: Optional[T] = None): 11 self._value = value 12 13 def is_just(self) -> bool: 14 return self._value is not None 15 16 def map(self, f: Callable[[T], U]) -> "Maybe[U]": 17 if self.is_just(): 18 return Maybe(f(self._value)) 19 return Maybe() 20 21 def join(self) -> "Maybe[T]": 22 if self.is_just() and isinstance(self._value, Maybe): 23 self._value = self._value._value 24 return self 25 26 def bind(self, f: Callable[[T], U]) -> "Maybe[T]": 27 return self.map(f).join() 28 29 30def safe_divide(a: float, b: float) -> Maybe[float]: 31 if b == 0: 32 return Maybe(None) 33 return Maybe(a / b) 34 35 36if __name__ == "__main__": 37 safe_divide_2 = partial(safe_divide, b=2) 38 value = Maybe(10) 39 result = value.bind(safe_divide_2) 40 print(result._value)`

This time, we use `bind`

method instead of `map`

method. It unwraps the nested `Maybe`

, making sure `result`

is of type `Maybe[int]`

as expected.

Today, we explored *monads* and their benefits in functional programming. Specifically, we learned about the `Maybe`

monad, how to check its state using `is_just`

, and how to handle operations properly using the `bind`

method.

Now that we've covered the theory and seen some examples, it's time to put your knowledge into practice. You'll work on exercises to help solidify your understanding of the `Maybe`

monad and its applications. Happy coding!