Monads in Python with PyMonad

Python has a good set of tools in its core library to do functional programming effectively. It does, though, lack certain tools out-of-the-box do deal with functional programming patterns like persistent data structures or Monads, but these can be found in third-party libraries that do have solid implementations for these.

In this post, we will take a look at (probably) the most mature library in Python with Monad data structures: PyMonad. PyMonad is an implementation of monad data structures in Python based on programming languages like haskell and F# with implementations for the most commonly used monads types in functional programming. We’ll tour through the existing monads in this library for version 2.3.5.

Monads and PyMonad

There are many design patterns designed for solving problems in functional programming, being monads one of them. Monads can be viewed as a sort of a container of values with special hooks which we can use to apply operations into an internal value and be able to chain a set of operations together.

Monad container

PyMonad is a Python implementation of monads that achieves such purpose. The following are the structures we’ll be taking a look at that are available in the pymonad library, their use-cases and how to use them:

  • Monad (Base class)
  • Maybe Monad
  • Writer Monad
  • Reader Monad
  • State Monad
  • Either Monad
  • List Monad
  • IO Monad
  • Promise Monad

Monad (Base class)

From PyMonad’s documentation [1]: “The Monad base class is an abstract class which defines the operations available on all monad instances.”

In a simplistic way, the Monad class is a wrapper around a value that contains the necessary logic to apply transformations into it, and it is the base class of the other monad types. Core methods like insert(), bind() and map() are not implemented since several monad types have a specific implementation for these, so there is not much you can do with the base class itself though.

>>> from pymonad.monad import Monad
>>> m = Monad('val', False)
>>> type(m)
<class 'pymonad.monad.Monad'>
>>> m.bind(lambda x: x.upper())
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/zay/anaconda3/envs/monad/lib/python3.8/site-packages/pymonad/monad.py", line 117, in bind
    raise NotImplementedError
NotImplementedError

In PyMonad, all monad classes have two attributes:

  • .value that stores the value of the monad;
  • .monoid which stores the ‘meta data’ part of the monad.
>>> m.value
'val'
>>> m.monoid
False

The .monoid attribute is used to define behaviours in monads like Maybe and Either, but it is an implementation detail and not something you actively need to use when dealing with PyMonad.

Maybe Monad

The Maybe monad is a commonly used data structure structure in FP. It allows to deal with undefined values gracefully when chaining a set of operations together.

For example, when you apply a function to a value, you may get different results depending on a context. If there is some undefined behaviour comming out of a chain of operations, the Maybe monad allows to deal with such undefined behaviour on a latter stage.

Think of it as a track line that can go in two different ways depending on the outcome of an operation like, for example, updating the state of a database if some action has been performed. If the action was a valid one, then we can proceed to update the database. Otherwise, you would deal with the invalid action in some other way (like skiping the oepration altogether).

Maybe monad as a railroad track

With this in mind, lets see this in action with an example using PyMonad’s Maybe class. First, lets initialize the object with a value:

>>> from pymonad.maybe import Maybe
>>> coord = Maybe({"x": 0, "y": 0}, True)
>>> coord
Just {'x': 0, 'y': 0}
>>> type(coord)
<class 'pymonad.maybe.Maybe'>

In this snippet, you could have defined this exact same object using the Just class because that is what the Maybe class defaults to if the second argument (monoid) is set to True:

>>> from pymonad.maybe import Just
>>> coord = Just({"x": 0, "y": 0})
>>> coord
Just {'x': 0, 'y': 0}

If the monoid input argument is set to False though, it becomes a Nothing monad:

>>> coord = Maybe({"x": 0, "y": 0}, monoid=False)
Nothing

The Nothing monad always returns the same value independently of whatever operation is mapped on top of it. Actually, if the Maybe monad state becomes Nothing, all operations are skipped.

So, in essence, the Maybe monad can be one of two possible states: Just (a value) or Nothing.

Nothing returns nothing!

If the outcome of an action of a value inside the maybe monad is valid, it turns into a Just state” which will continue to pass its state into a chain of transformations. If the outcome is not valid, it becomes a Nothing value and it will just bypass the chain of transformations entirely.

Nothing returns nothing!

Say you want to implement the translation movement through a 2D space in a game for a player and impede the player from getting out-of-bounds whenever he moves around. Because there are positions on a 2D grid that are not valid (i.e., out-of-bounds), we need to make sure that a set of player’s actions cannot go outside the limits imposed by the game. To achieve this, you need to define two operations: moving around and validating the player’s coordinates.

First, lets define the operation of moving around with a simple translation: receive the current coordinates and apply a vector to it.

def translation(coords: dict, vector: dict):
    print(f"Coordinates: {coords} | Translation vector: {vector}")
    new_coords = {
        "x": coords["x"]  + vector["x"],
        "y": coords["y"]  + vector["y"],
    }

    if (new_coords["x"] >= 0) and  (new_coords["y"] >= 0) and (new_coords["x"] <= 10) and  (new_coords["y"] <= 10):
        return new_coords  # in-bounds
    else:
        return Nothing     # out-of-bounds

Second, we will contrain the possible movements to the following ones just to simplify things for this example:

>>> from functools import partial
>>> move_up = partial(translation, vector={"x": 0, "y": 1})
>>> move_down = partial(translation, vector={"x": 0, "y": -1})
>>> move_left = partial(translation, vector={"x": -1, "y": 0})
>>> move_right = partial(translation, vector={"x": 1, "y": 0})

We can test these functions to see if it the translation method works as expected:

>>> coord.then(move_up) \
...      .then(move_right) \
...      .then(move_down) \
...      .then(move_left)
Coordinates: {'x': 0, 'y': 0} | Translation Vector: {'x': 0, 'y': 1}
Coordinates: {'x': 0, 'y': 1} | Translation Vector: {'x': 1, 'y': 0}
Coordinates: {'x': 1, 'y': 1} | Translation Vector: {'x': 0, 'y': -1}
Coordinates: {'x': 1, 'y': 0} | Translation Vector: {'x': -1, 'y': 0}
Coordinates: {'x': 0, 'y': 0} | Translation Vector: {'x': 0, 'y': -1}

Looks good.

Now, when we are moving in the permited coordinate plane, all goes according to the plan. But what happens if we cross the forbidden zone?

>>> coord.then(move_up) \
...      .then(move_right) \
...      .then(move_down) \
...      .then(move_left) \
...      .then(move_left)  # Move to a restricted zone
Coordinates: {'x': 0, 'y': 0} | Translation Vector: {'x': 0, 'y': 1}
Coordinates: {'x': 0, 'y': 1} | Translation Vector: {'x': 1, 'y': 0}
Coordinates: {'x': 1, 'y': 1} | Translation Vector: {'x': 0, 'y': -1}
Coordinates: {'x': 1, 'y': 0} | Translation Vector: {'x': -1, 'y': 0}
Coordinates: {'x': 0, 'y': 0} | Translation Vector: {'x': 0, 'y': -1}
Nothing

As expected, we get a Nothing object. If you continue attempting to move back to an allowed coordinate in the 2D plane, you will notice that you are unable to do so because Nothing is being propagated throughout the chain of operations downstream, ands all the operations applyed afterwards will be bypassed.

This may or may not be the desired case for a real-world case, but for this example, this is what we want.

>>> coord.then(move_up) \
...      .then(move_right) \
...      .then(move_down) \
...      .then(move_left) \
...      .then(move_left) \
...      .then(move_right) \  # Attempting to get
...      .then(move_right) \  # out of this plane
...      .then(move_up)       # is futile
Coordinates: {'x': 0, 'y': 0} | Translation Vector: {'x': 0, 'y': 1}
Coordinates: {'x': 0, 'y': 1} | Translation Vector: {'x': 1, 'y': 0}
Coordinates: {'x': 1, 'y': 1} | Translation Vector: {'x': 0, 'y': -1}
Coordinates: {'x': 1, 'y': 0} | Translation Vector: {'x': -1, 'y': 0}
Coordinates: {'x': 0, 'y': 0} | Translation Vector: {'x': 0, 'y': -1}
Nothing

We can see that this can come in handy to avoid some hazardous scenarios down a chain of events. Of course that you will need to do some additional validation after the set of operations is complete, but, in the meantime, you can skip doing checks on every function you defined and let the Maybe Monad handle the undefined logic for you at this stage.

To see the full example in action, lets define a database update method and move the player around a bit:

db = {}
def update_database(coords: dict) -> None:
    db["x"] = coords["x"]
    db["y"] = coords["y"]
    print(f"New db coordinates: {db}\n")
>>> db = {"x": 0, "y": 0}
>>> Maybe.insert(db).then(move_up).then(update_database)
>>> Maybe.insert(db).then(move_down).then(update_database)
>>> Maybe.insert(db).then(move_down).then(update_database)
>>> Maybe.insert(db).then(move_down).then(update_database)
>>> Maybe.insert(db).then(move_down).then(update_database)
>>> Maybe.insert(db).then(move_down).then(update_database)
>>> Maybe.insert(db).then(move_right).then(update_database)
>>> Maybe.insert(db).then(move_up).then(update_database)
>>> Maybe.insert(db).then(move_right).then(update_database)
>>> Maybe.insert(db).then(move_down).then(update_database)
>>> Maybe.insert(db).then(move_right).then(update_database)
>>> Maybe.insert(db).then(move_right).then(update_database)
Coordinates: {'x': 0, 'y': 0} | Translation vector: {'x': 0, 'y': 1}
New db coordinates: {'x': 0, 'y': 1}

Coordinates: {'x': 0, 'y': 1} | Translation vector: {'x': 0, 'y': -1}
New db coordinates: {'x': 0, 'y': 0}

Coordinates: {'x': 0, 'y': 0} | Translation vector: {'x': 0, 'y': -1}
Coordinates: {'x': 0, 'y': 0} | Translation vector: {'x': 0, 'y': -1}
Coordinates: {'x': 0, 'y': 0} | Translation vector: {'x': 0, 'y': -1}
Coordinates: {'x': 0, 'y': 0} | Translation vector: {'x': 0, 'y': -1}
Coordinates: {'x': 0, 'y': 0} | Translation vector: {'x': 1, 'y': 0}
Coordinates: {'x': 0, 'y': 0} | Translation vector: {'x': 1, 'y': 0}
New db coordinates: {'x': 1, 'y': 0}

Coordinates: {'x': 1, 'y': 0} | Translation vector: {'x': 0, 'y': 1}
New db coordinates: {'x': 1, 'y': 1}

Coordinates: {'x': 1, 'y': 1} | Translation vector: {'x': 1, 'y': 0}
New db coordinates: {'x': 2, 'y': 1}

Coordinates: {'x': 2, 'y': 1} | Translation vector: {'x': 0, 'y': -1}
New db coordinates: {'x': 2, 'y': 0}

Coordinates: {'x': 2, 'y': 0} | Translation vector: {'x': 1, 'y': 0}
New db coordinates: {'x': 3, 'y': 0}

Coordinates: {'x': 3, 'y': 0} | Translation vector: {'x': 1, 'y': 0}
New db coordinates: {'x': 4, 'y': 0}

Just None

Any attempt to go “out-of-bounds” gets restricted, and you can only move if the global state is in the valid coordinate range :).

In this example, an advantage of using the Maybe monad is to simplify dealing with invalid behaviour. This enabled us to write simpler functions that don’t need to be aware of the context outside of their scope.

Additionally, we can deal with the outcome of the set of operations in some intended way afterwards, but not having to deal with it explicitely at every step of they way is a major QoL improvement.

Writer Monad

The Writer monad represents computations which produce a stream of data in addition to the computed values.

Writer monad in action

The Writer monad allows you to append extra information like, for example:

  • Log messages along side the data you are processing;
  • Append metadata to a dictionary;
  • Construct a list of the name of the functions and in what order they were executed.

How can we use the Writer monad to accomplish this? Well, lets take the following example to see how it works under the hood and lets make something with it to, say, automatically log what operation was executed.

The goal here is to make a simple string transformation pipeline that outputs a transformed string and that logs the function name to the output.

First, lets see how pymonad.writer.Writer works. In its essence, it works as the following: a) it receives value as input and a monoid; then b) operations on the monad will execute operations on both the value and the monoid attributes.

The pymonad.writer.Writer receives as input two parameters: 1) a generic value and 2) a monoid which can be of any generic type (it can be a class).

>>> from pymonad.writer import Writer
>>>
>>> Writer('Some value', 'Some text')
(Some value, Some text)
>>> Writer('Some value', 42)
(Some value, 42)
>>> Writer('Some value', lambda x: x + 1)
(Some value, <function <lambda> at 0x7effd3d10e50>)

It returns an object of type Writer with two attributes: value and monoid.

>>> w = Writer('Some value', 'Some text')
>>> w.value
'Some value'
>>> w.monoid
'Some text'
>>> type(w)
<class 'pymonad.writer.Writer'>

When applying some operation on top of this object, for example upper casing all characters, this is what we get:

>>> w.map(lambda x: x.upper())
(SOME VALUE, Some text)

Mapping a simple function over the Writer object just applies the mapping operation on the value part. But with the Writer monad you want not only apply a computation over a value but also append information (e.g., log stuff) along the way. For that, you need to modify the previous mapping function and use the .bind() method instead and return a Writer object:

>>> w.bind(lambda x: Writer(x.upper(), f"Upper case operation done on: {x}"))
(SOME VALUE, Some textUpper case operation done on: Some value)

With this process, you can start using pymonad.writer.Writer data structure effectively.

Additional note on pymonad’s Writer usage

The operation the Writer applies on the monoid attribute is an add() operation. With this, we can overload any object’s add() method and, lets say, append log messages to a list and output the log to sysout.

import logging

class MyLog(object):

    def __init__(self, val):
        if isinstance(val, list):
            self.val = val
        else:
            self.val = [val]

    def __add__(self, val):
        logging.info(val)
        if isinstance(val, list):
            result = self.val + val
        else:
            result = self.val + [val]
        return MyLog(result)

    def __repr__(self):
        return f"Mylog({self.val})"

    def __str__(self):
        return f"Mylog({self.val})"

MyLog() will append values into a list and automatically log it to sysout, but the concept of appending state remains for the writer monad.

>>> Writer('Some value', MyLog('Process string: "Some value"'))\
...    .bind(lambda x: Writer(x.upper(), "Uppercase string"))\
...    .bind(lambda x: Writer(x.split(" "), "Split string"))
INFO:root:Uppercase string
INFO:root:Split string
(['SOME', 'VALUE'], Mylog(['Process string: "Some value"', 'Uppercase string', 'Split string']))

Reader Monad

The Reader monad creates a context in which functions have access to an additional read-only input. In a chain of computations, this read-only input is passed to the first function, and the result of the first function is passed to the second function, which result its result is passed to the third function, and so on.

The Reader monad is commonly used for passing (implicit) configuration information through a computation. Whenever you have a “constant” in a computation that you need at various points, but really you would like to be able to perform the same computation with different values, then you should use a reader monad [2].

Reader monad step-by-step

PyMonad’s Reader data structure expects a function as input for class initialization. This function will return a data structure when calling the monad with either .__call__() or .value(arg) and send it to the bind/map operation if it is the case.

>>> from pymonad.reader import Reader
>>> Reader(lambda x: [1, 2])
<pymonad.reader._Reader at 0x7fa3d2e00bd0>
>>> Reader.insert([1, 2])        # same as the above
<pymonad.reader._Reader at 0x7fa3d2fd8390>
>>>
>>> Reader.insert([1, 2]).value(None)
[1, 2]
>>> Reader.insert([1, 2])(None)  # same as the above
[1, 2]

The way to retrieve the value is a bit odd for the Reader class but, because the Pipe and Compose classes are based on the Reader class, they are more commonly used in pymonad then the Reader class itself to chain operations on a value or to compose a function from other functions, the way it is implemented makes sense.

Thus, when initializing with static data structures like in the previous example, we need to do that extra step and call the object with None or something similar.

Lets take a look at the reader class definition of the __call__() method. This is how it is implemented in PyMonad:

class _Reader(pymonad.monad.Monad, Generic[R, T]):
...
    def __call__(self, arg: R) -> T:
        return self.value(arg)

It returns the contents of the value attribute (which is defined as a function in the Reader class) and applies an argument to it. The way it is implemented was to enable composition of functions (we’ll see it shortly) into a single function and allow to call it with an input parameter, but this mechanism also enables us to do something more fancy with it.

For example, we can pass a function which retrieves the value of a dictionary when initializing the reader monad, and allow to fetch different sets keys from that dictionary and pass it off as state to a function.

>>> d = {"a": 1, "b": 2}
>>> r = Reader(lambda x: d[x])
>>> r.map(lambda x: x*2).value("a")
2
>>> r.map(lambda x: x*2).value("b")
4

Additionally, pymonad.reader module has two usefull methods that make using the Reader monad easier: Pipe and Compose. Both are basically an alias for the Reader monad except with the insert and apply methods removed.

Pipe purpose is simply to provide a way of chaining function calls by taking the output of one function as the input to the next.

>>> def inc(x): return x + 1
>>> pipe_with_flush = (Pipe(0)
...                  .then(inc)
...                  .then(inc)
...                  .flush())
>>> pipe_with_flush
2

Compose purpose is to provide a way of composing functions together.

>>> def inc(x): return x + 1
>>> def dec(x): return x - 1
>>> convoluted_inc_twice = (Compose(inc)
...                         .then(inc)
...                         .then(inc)
...                         .then(dec))
>>> convoluted_inc_twice(0)
2

And, in a nutshell, this is what the Reader monad is used for.

State Monad

The State monad is the result of merging the functionality of the Reader and Writer monads into a single entity. Thus, theState monad contains an immutable state that is passed through a chain of computations, and the output gets a new state appended to it. It enables us to manage the state of a program effectively in a functional way.

State monad step-by-step

Setting up the State monad is similar to the Reader monad: you pass a function as the input state.

>>> from pymonad.state import State
>>> State(lambda x: ([1, 2], x))
<pymonad.state.State at 0x7ffaa7a8a390>
>>> State.insert([1, 2])        # same as the above
<pymonad.state.State at 0x7ffaa7dbbcd0>

The difference is that the input function needs also to return an additional output which will be the new state, which will be appended to the output of the chain of computions:

>>> State.insert([1, 2]).then(lambda x: sum(x)).run('A')
(3, 'A')
>>> State.insert([1, 2]).then(lambda x: sum(x)).value('A')  # same as the above
[1, 2]

We use the State monad whenever we need to pass a state between several functions without the cumbersomeness of explicitly passing a value through all functions. This use case may be something you do alot or something that you do rarely. Whatever which one is, you can save some work by using the State monad.

Either Monad

The PyMonad’s documentation explains really well what the Either monad is: “The Either type represents values that can either type A or type B - for any types A and B - but not both at the same time. As a function input type, Either values can be used to define functions which can sensibly deal with multiple types of input; of course in python we don’t need a special way to deal with multiple input types.

Perhaps more usefully, as an output type, Either values can be used to signal that a function may cause an error: Either we get back a useful result or we get back an error message.”

The Either monad is used often to handle errors in a non-blocking way, or to define a default value for a set of computations.

It is similar to the Maybe monad, but it offers a more explicit way to deal with undefined or unwanted behaviour. Using the same analogy as in the Maybe Monad, we can think of the Either type as a railroad track splitting into two possible paths: the intended (good) path; and the exception (sad) path.

Either monad as a railroad track

Instanciating the Either monad can be done in a few different ways:

>>> from pymonad.either import Either, Left, Right
>>> Either(10, (None, True))
Right 10
>>> Either(10, (0, True))
Right 10
>>> Either(10, (None, False))
Left None
>>> Either(10, (0, False))
Left 0
>>> Either.insert(10)
Right 10
>>> Right(10)
Right 10
>>> Left(10)
Left 10

Lets make a concrete example for the Either monad: in a chain of computations, if a “ZeroDivisionError” error occurs, we manage what error message is given and deal with it in a graceful way.

>>> from pymonad.tools import curry
>>>
>>> add = curry(2)(lambda x,y: x + y)
>>> mul = curry(2)(lambda x,y: x * y)
>>> div = curry(2)(lambda x,y: x / y if y > 0 else Left(f"Divide by zero found: x: {x}, y: {y}"))
>>> e = (Either.insert(5)
               .then(mul(2))
               .then(add(-10))
               .then(div(5))  # Returns a Left with a string
               .then(add(100))
>>> e
Left Divide by zero found: x: 5, y: 0

The Either monad allows us to specify a method to apply to either paths of the computation, depending on the outcome of them (i.e., if it returns an either right or left monad). This is quite handy for defining an action for any of the outcomes and to proceed to execute additional computations if desired on a set of computations:

>>> e = (Either.insert(5)
               .then(mul(2))
               .then(add(-10))
               .then(div(5))  # Returns a Left with a string
               .then(add(100))
               .either(
                    lambda e: f"Error string | {e}",
                    lambda x: x) # ignore the error and return the value x
               )
>>> e
'Error string | Divide by zero found: x: 5, y: 0'

We can also define a value for the left path of the chain of computation like the following:

>>> e = (Either.insert(5)
               .then(mul(2))
               .then(add(-10))
               .then(div(5))  # Returns a Left with a string
               .then(add(100))
               .either(
                    lambda e: 0,
                    lambda x: x) # ignore the error and return the value x
               )
>>> e
0

This brings a great deal of customization for undefined behaviour compared to the Maybe monad.

Maybe Monad vs Either Monad

These two are somewhat similar in their execution, where the major difference is that the Maybe monad either returns a value or nothing (None), while the Either monad allows you to specify a value instead for the “other” path.

Both have use cases. This great response on stackoverflow [1] helps understand the general use case for both monads:

  • Maybe is one (value) or none – ie, you have a value or you have nothing

  • Either is a logical disjunction, but you always have at least one (value) - ie, you have one or the other, but not both.

Maybe is great for things like where you may or may not have a value - for example looking for an item in a list. if the list contains it, we get (Maybe x) otherwise we get Nothing

Either is the perfect representation of a branch in your code - it’s going to go one way or the other; Left or Right.

List Monad

From the documentation: “The List monad is frequently used to represent calculations with non-deterministic results, that is: functions which return more than one (possible) result.”

Its use-case revolves around applications where the number of elements in a series os computations varies between stages (e.g., in a data processing pipeline with filters applyed to an array of elements).

List Monad as an array of values

Using the List monad from pymonad comes as one would expect:

>>> from pymonad.list import ListMonad
>>> ListMonad(1,2,3)
[1, 2, 3]
>>> type(ListMonad(1,2,3))
pymonad.list._List
>>> ListMonad(1,2,3).value
[1, 2, 3]
>>> type(ListMonad(1,2,3).value)
list

Note: The List monad in pymonad (ListMonad) is the only one with a suffix attached to it (most likely to avoid confusion with the built-in list object… good idea!).

No surprises here. You can access any element on the ListMonad object as you would on a regular Python list:

>>> ListMonad(1,2,3)[0]
1
>>> type(ListMonad(1,2,3)[0])
int

Applying a method over a list monad will result in applying the method for each individual element on the list. For example, adding one to each element of the list returns the following:

>>> ListMonad(1,2,3).map(lambda x: x + 1)
[2, 3, 4]

If a method returns more than one element, this is what you get:

>>> ListMonad(1,2,3).then(lambda x: [x, x + 1] if x > 1 else x)
[1, [2, 3], [3, 4]]

This would be the expected outcome for a Python list, but that is not how you want your list monad to work. You want a flat list so you can iterate over each element in separate, and down to the next function in the chain of computations.

To do that, just wrap the output of the method with the ListMonad class:

>>> ListMonad(1,2,3).then(lambda x: ListMonad(x, x + 1) if x > 1 else ListMonad(x))
[1, 2, 3, 3, 4]

This returns a flat list which you can continue operating on.

Additionally, if you want to filter elements from a list, you can do this by returning an empty list (identity) and the element will be removed from the output list monad:

>>> ListMonad(1,2,3).then(lambda x: ListMonad(x) if x > 1 else ListMonad())
[2, 3]

IO Monad

The IO monad deals is primarly “designed” to deal with side effects (but this can be achieved with the Reader, Writer or State Monads as well). The side effects can be anything from reading the contents of a file or printing information to the screen.

Initializing the IO monad requires you to pass a function to execute an io operation:

>>> from pymonad.io import IO
>>> IO(lambda: "Some value")
<pymonad.io._IO at 0x7f0118507310>
>>> IO(lambda: "Some value").value
<function __main__.<lambda>()>
>>> IO.insert("Some value").value  # .insert() returns a similar output
<function pymonad.io._IO.insert.<locals>.<lambda>()>

It requires you to execute the .run() method in order to retrieve the value from the monad.

>>> IO(lambda: "Some value").run()
'Some value'

For a more concrete example, say you need to retrieve information from a user via the keyboard, apply some operation over the input and return it. This is how you could do this with the pymonad’s IO data structure:

>>> IO(lambda: input("Insert text to be uppercased: "))\
...    .map(lambda x: x.upper())\
...    .run()

Typing something like this is a test will return THIS IS A TEST as the output.

As additional information, an interesting take on the usefulness of the IO monad can be found here which I found useful and I suggest the reader to take a look at with the misconceptions around what the IO Monad is (mostly Haskell related) and what its purpose is.

Note: If you need the IO monad in your code or not can be debatable, but if you need a procedure to deal with side effects, you can use the IO monad to accomplish this.

Promise Monad

From the documentation: “The Promise monad is based on (and named after) Javascript’s Promise objects and function in a similar way. Promises take asynchronous computations and ensure the ordering of execution. In addition to the standard operations on monads, Promises also provide a ‘catch’ method which allows for recovery from errors.”;

The Promise monad is used for the modeling of asynchronous operations, which are mostly related to IO (it frequently has to wait on previously retreated information). For example, there is an asynchronous call to a remote API, which gets information and utilizes it to subsequent requests, which is based on the prior result.

The use / initialization of the Promise monad is a bit different than the others, but it is not that much different. For instance, with the Reader monad, initialization is quite straight-forward: you initialize the class with a value and you retrieve it with the .value argument.

>>> from pymonad.reader import Reader
>>> Reader(10)
<pymonad.reader._Reader at 0x7fe09b378c10>
>>> Reader(10).value
10

With the Promise monad you need to do some extra work, but it does not deviate too far off from the norm. For the initialization step, to add a value or set of values, you need to pass it through a function with two args: resolve and reject.

>>> from pymonad.promise import Promise
>>> Promise(lambda resolve, reject: resolve(10))
<pymonad.promise._Promise at 0x7fe09b33b950>

This requires a bit of an explanation to make things clearer for the reader to understand the need of this setup.

The ‘resolve’ callback can take a value of any type but the ‘reject’ callback should always take an Exception as its argument.

When the computation is successful, the value should be returned by calling resolve() with the result. If there is an error, call reject() with an instance of the Exception class.

These two callbacks are key for handling errors caused in the Promise chain. The catch() method takes an error handling function as input. If an earlier computation in the promise chain has caused an error, either by being passed an Exception via the ‘reject’ callback or by an Exception being raised normally, then error handler is called with the Exception as an argument. If no error was previously raised, the error handler is ignored.

This procedure is simplified if you use the insert() method instead of the class __init__():

>>> from pymonad.promise import Promise
>>> Promise.insert(10)
<pymonad.promise._Promise at 0x7fe09b33b950>

To retrieve the value, however, you need to call it with await instead of just fetching the value with the .value attribute as other monads do. Thus, to retrieve the value, you need to do it in an async loop:

>>> import asyncio
>>> async def main():
>>>     return await Promise.insert(10)
>>> asyncio.run(main())
10

Note: If you are running this on a jupyter notebook, you can simply call await Promise.insert(10) without the asyncio loop. The use of a function is just a trick to fetch data from the promise object.

Mapping functions to the Promise monad is the same as the other monads (besides the little async/await dance we do here).

>>> async def main():
>>>     return await Promise.insert(10).map(lambda x: x + 1)
>>> asyncio.run(main())
11

The example from the documentation shows how you can use the Promise monad effectively and how to deal with exceptions on the promise chain. Here is the code:

from pymonad.tools import curry

@curry(2)
def div(y, x):
    return x / y

async def long_id(x):
    await asyncio.sleep(1)
    return await Promise(lambda resolve, reject: resolve(x))

async def main():
    x = (Promise.insert(1)
                .then(long_id))
    y = (Promise.insert(2)
                .then(long_id)
                .then(div(0))            # Raises an error...
                .catch(lambda error: 2)) # ...which is dealth with here.
    print(
        await Promise.apply(add)
                     .to_arguments(x, y)
                     .catch(lambda error: 'Recovering...') # This is ignored
                                                           # because the previous
                                                           # catch already dealt
                                                           # with the error.
    )

asyncio.run(main())

This prints the value ‘3’ to the screen. The long_id coroutine is a stand-in for any async operation that may take some amount of time. When we await the promise inside the print() call it waits for both arguments to complete before calling ‘add’ with the results. If the first call to catch were removed then the error would propagate and be caught by the second call. The program would then print the string `Recovering…’ instead of ‘3’.

Conclusion

PyMonad provides a usable set of monad implementations that will help you to develop clearer code in a saner way. The implementation details are easy to grasp and the code is very understandable from a user’s point of view.

The set of monads available out-of-the-box in the library is provide the necessary funcionality to help you deal with a varied set of design patterns you will hit when developing a solution for a problem. The most important monad types are present and ready to be used: Maybe, Reader, Writer, State, Either, Promise and IO.

Furthermore, the library is actively being supported and improved, which should help its adoption and gather further improvements from the Python’s functional community.

If you haven’t used pymonad but would like to give it a try, go ahead and install the latest version available (2.3.5+) and go have some fun!

Cheers

Sources and useful links

[1] https://stackoverflow.com/questions/42554353/why-do-we-need-maybe-monad-over-either-monad

[2] https://stackoverflow.com/questions/14178889/what-is-the-purpose-of-the-reader-monad

[3] https://jrsinclair.com/articles/2019/elegant-error-handling-with-the-js-either-monad/

[4] https://blog.jle.im/entry/io-monad-considered-harmful.html