Back to Posts
Close-up of eyeglasses focused on computer screens displaying code, illustrating the concept of Python functors and monads for efficient programming.

Python Functors and Monads: A Practical Guide

Have you ever seen a more alien phrase? I know I haven’t, but I’m here to tell you that it’s not as complicated as it seems. Let’s probe what it means when we say these terms and how you can take inspiration from functional concepts to improve the robustness of your code. Monads provide the perfect tools for encapsulating data pipelines in a safe, predictable way.

What is an endofunctor?

In short, an endofunctor is a way to containerize a value and allow a series of transformations upon that value. However, do note that functors have a special property where they always return the same type as the input, for example, if a string was input, then the return would also be a string.

Endofunctors are most commonly seen and used in functional programming, and while uncommon in Python, they can effectively be applied in Python to allow for clean, concise pipelines.

Let me show you what a functor looks like in Python:

class Functor(Generic[T]):
    def __init__(self, value: T):
        self.value = value

    def map(self, f: Callable[[T], U]) -> "Functor[U]":
        return Functor(f(self.value))

A functor contains two main components: a value and a method in which to apply functions to said value. As you can see by the map method, it always returns a Functor, which adheres to the special property I mentioned earlier.

def add_1(x: int) -> int:
    return x + 1

def square(x: int) -> int:
    return x * x

 print(Functor(1).map(add_1).map(square).value)  # 4

Observe how Functors enable the chaining of a sequence of transformations on the encapsulated value. Initially, the value undergoes an addition of 1, followed by squaring. It’s noteworthy that the functions involved don’t require awareness of the monad itself; instead, they can focus on the contained value.

But wait, then, what is a monad?

A monad is a type of functor that usually handles special concerns in the mapping process; this is often for things like exception handling. Monads are very useful for controlling how we apply functions to a value and in what context.

Let’s take a look at another example of a monad, the Maybe Monad:

class Maybe(Generic[T]):
    def __init__(self, value: T | None):
        self.value = value

    def map(self, f: Callable[[T], U]) -> "Maybe[U]":
        if self.value is None:
            return self
        return Maybe(f(self.value))

In this illustration, we showcase the Maybe Monad. Similar to the Functor class introduced earlier, the Maybe Monad adheres to a familiar pattern but introduces a type of constraint to the mapping process. Specifically, the mapping operation is executed only if the encapsulated value is not None.

This proves particularly valuable in scenarios where a sequence of operations relies on a certain value, yet there is a possibility that this value might be absent. The Maybe Monad thus provides a structured approach to handling optional values within a computational pipeline.

def safe_div(x: float, y: float) -> Union[float, None]:
   if y == 0:
      return None
   return x / y

def sub_one(x: float) -> float:
     return x - 1

def add_one(x: float) -> float:
     return x + 1

print(Maybe(1).map(sub_one).map(safe_div).map(add_one).value) # None

As the value approaches zero, the subsequent operations cease to be applied, resulting in a consequential output of None.

This type of conditional handling is valuable in situations where the chain of operations requires data that may be unavailable or non existent, for example collecting a value from a database. when confronted with a None value, it is handled gracefully and no further operations are applied

class Result(Generic[T]):
    def __init__(self, value: T | Exception):
        self.value = value

    def map(self, f: Callable[[T], U]) -> "Result[U | Exception]":
        if isinstance(self.value, Exception):
            return self
        try:
            return Result(f(self.value))
        except Exception as e:
            return Result(e)

Introducing another instance of a monad designed to manage potential failures: the Result Monad. In contrast to the Maybe Monad, the Result Monad takes a more explicit approach to handling exceptions rather than relying on a None value. This explicit handling offers additional context regarding the cause of the failure, making it particularly advantageous in scenarios similar to those addressed by the Maybe Monad.

This pattern proves notably beneficial in application programming interfaces (APIs) and other data retrieval systems where the data originates from external sources. The Result Monad enhances error management by providing detailed insights into why a particular operation might have failed, enhancing the robustness of systems that interact with external data.

As evident from the examples, each of these monads facilitates the creation of function chains, employing a paradigm often referred to as a “railroad approach.” This approach visualizes the sequence of functions as a metaphorical railroad track, where the code smoothly travels along, guided by the monadic structure. The beauty of this railroad approach lies in its ability to elegantly manage complex computations and transformations, ensuring a structured and streamlined flow of operations.

An intersection of math and language

These concepts were initially introduced by Eugenio Moggi, a computer scientist affiliated with the University of Genoa in Italy. In his seminal paper titled Notions of Computation and Monads, Moggi delves into various approaches for encapsulating side effects within functional languages across different contexts. His overarching objectives were to establish connections between set theory and functional programming, provide a mechanism for segregating concerns, and exert influence on the language of functional programming.

Moggi’s work laid a foundational framework for understanding and implementing monads, significantly impacting the evolution of functional programming languages.

Final thoughts

I hope by reading this article, the phrase monads are monoids in the category of endofunctors has more meaning and that I have demonstrated how they can be a useful tool in the toolbox of the software developer.

By utilizing these monads, developers can construct robust and easily comprehensible pipelines, enhancing the clarity and maintainability of their code.

If you wish to watch my video on the subject, check out: What the Heck Are Monads?!

Improve your code with my 3-part code diagnosis framework

Watch my free 30 minutes code diagnosis workshop on how to quickly detect problems in your code and review your code more effectively.

When you sign up, you'll get an email from me regularly with additional free content. You can unsubscribe at any time.

Recent posts