fp.monads — A collection of monads

New in version 0.1.

the fp.monads module provides a collection of monads to use with your Python code.

Maybe

fp.monads provides an implementation of the Maybe monad.

class fp.monads.maybe.Maybe(value)

A Maybe monad.

The Maybe monad is helpful when dealing with sequences of functions that could return None.

For instance, when fetching a key from a dictionary that may not be there:

>>> from fp.monads.maybe import Maybe, Just, Nothing
>>> data = {"foo": {"bar": {"baz": "bing"}}}
>>> data['foo']['bong']['baz']
Traceback (most recent call last):
    ...
KeyError: 'bong'

This is a very common problem when processing JSON. Often, with JSON, a missing key is the same as null but in Python a missing key raises an error.

dict.get is often the solution for missing keys but is still not enough:

>>> data.get("foo").get("bong").get("baz")
Traceback (most recent call last):
    ...
AttributeError: 'NoneType' object has no attribute 'get'

To final solution to this ends up being ugly:

>>> data.get("foo", {}).get("bong", {}).get("baz") is None
True

It is even more complex when dealing with mixed types; for instance if “bong” ends up being a list instead of a dict.

The Maybe monad lets us express errors such as these as either something or nothing. This is much like dict.get() returning None, but we can chain Maybe actions so that they fail with Nothing, if any of the functions in the chain returns Nothing.

Normally getitem will raise an error if the key or index is invalid, Maybe.catch() causes Nothing to be returned if any exception occurs.

>>> from operator import getitem
>>> from fp import p
>>> lookup = p(Maybe.catch, getitem)
>>> lookup({"foo": "bar"}, "foo")
Just('bar')
>>> lookup({"foo": "bar"}, "bong")
Nothing

Now lookup returns Just(x) if the key is found or Nothing if the key is not found.

To extract the value out of the Just() class, we can call Maybe.default():

>>> lookup({'foo': 'bar'}, 'foo').default('')
'bar'
>>> lookup({'foo': 'bar'}, 'bong').default('')
''

To chain the lookups, we simply need to partially apply the lookup function:

>>> from fp import pp
>>> lookup(data, "foo").bind(pp(lookup, "bar")).bind(pp(lookup, "baz"))
Just('bing')
>>> lookup(data, "foo").bind(pp(lookup, "bong")).bind(pp(lookup, "baz"))
Nothing

This is still not as pretty as it could be so we provide a get_nested function in the fp.collections module:

>>> from fp.collections import get_nested
>>> get_nested(Maybe, data, "foo", "bar", "baz")
Just('bing')
>>> get_nested(Maybe, data, "foo", "bong", "baz")
Nothing

In addition, functions that return None become Nothing automatically:

>>> Maybe.ret(None)
Nothing
>>> Maybe.ret({}.get('foo'))
Nothing

This feature allows you to easily integrate the Maybe monad with existing functions that return None.

bind(f)

The Maybe’s bind function. f is called only if self is a Just.

>>> Just(1).bind(lambda x: Maybe(x))
Just(1)
>>> def crashy(x):
...     assert False, "bind will not call me"
>>> Nothing.bind(crashy)
Nothing
classmethod cat_maybes(maybes)

Returns an iterator of the Just values

>>> list(Maybe.cat_maybes([
...     Just(1), Nothing, Just(2)
... ]))
[1, 2]
default(default_value)

Returns the just value or the default value if Nothing

>>> Just(1).default(-1)
1
>>> Nothing.default(-1)
-1
classmethod fail(err)
>>> Maybe.fail("some error")
Nothing
from_just

Extracts the just value from the Maybe; raises a ValueError if the value is None

>>> Just(1).from_just
1
>>> Nothing.from_just
Traceback (most recent call last):
    ...
ValueError: Maybe.from_just called on Nothing
is_just

Tests if the Maybe is just a value

>>> Just(1).is_just
True
>>> Nothing.is_just
False
is_nothing

Tests if the Maybe is Nothing

>>> Nothing.is_nothing
True
>>> Just(1).is_nothing
False
classmethod map_maybes(f, xs)

maps a list over a Maybe arrow

>>> def maybe_even(x):
...     if x % 2 == 0:
...         return Just(x)
...     else:
...         return Nothing
>>> list(Maybe.map_maybes(maybe_even, [1,2,3,4]))
[2, 4]
mplus(y)

An associative operation.

>>> Just(1).mplus(Maybe.mzero)
Just(1)
>>> Nothing.mplus(Maybe(2))
Just(2)
>>> Just(1).mplus(Just(2))
Just(1)

Either

fp.monads provides an implementation of the Either monad.

class fp.monads.either.Either

The Either monad is helpful when dealing with sequences of functions that can fail. These functions return Right(value) on success and Left(error) on failure.

When chained together, these functions will short circuit the chain when a failure occurs.

For instance an example of lookup(dict, key) could be:

>>> def lookup(d, k):
...     try:
...         return Right(d[k])
...     except Exception as err:
...         return Left(err)
>>> lookup({'foo': 'bar'}, 'foo')
Right('bar')
>>> lookup({}, 'foo')
Left(KeyError('foo',))

The benefit of Either’s over Exceptions is that you’re forced to handle the error eventually. You can’t let the Exception bubble to the surface and crash your program. Nothing escapes the monad!

The Either method lets you handle the error in your own way.

>>> lookup({}, 'foo').either(
...     lambda err: err,
...     lambda val: val
... )
KeyError('foo',)
>>> lookup({'foo': 'bar'}, 'foo').either(
...     lambda err: err,
...     lambda val: val
... )
'bar'

The default method lets you completely ignore the error:

>>> lookup({'foo': 'bar'}, 'foo').default('bing')
'bar'
>>> lookup({}, 'foo').default('bing')
'bing'

Just like any other monad, functions can be chained together using bind(f):

>>> lookup({'foo': {'bar': 'baz'}}, 'foo').bind(
... lambda foo: lookup(foo, 'bar'))
Right('baz')
>>> lookup({'foo': {}}, 'foo').bind(
... lambda foo: lookup(foo, 'bar'))
Left(KeyError('bar',))
>>> lookup({}, 'foo').bind(
... lambda foo: lookup(foo, 'bar'))
Left(KeyError('foo',))
default(default)

Returns the value if right, default value if left.

either(left_fun, right_fun)

Execute the left_fun(err) on Left, right_fun(value) on Right

is_left()

True if the Either is left

is_right()

True if the Either is right

classmethod lefts(eithers)

Extract the lefts from the list

>>> list(Either.lefts([Left(1), Right(2), Left(3)]))
[Left(1), Left(3)]
classmethod rights(eithers)

Extract the rights from the list

>>> list(Either.rights([Left(1), Right(2), Left(3)]))
[Right(2)]

IO

fp.monads provides an implementation of the IO monad.

class fp.monads.iomonad.IO(action)

This is the IO monad. Useful in composing IO code

This module implements an IO monad. Its usefulness in Python can be argued against (and won easily) but it is implement for completeness.

>>> @io
... def printLn(x):
...     print(x)

The IO action is returned when calling printLn but nothing is printed

>>> action = printLn("hi")
>>> isinstance(action, IO)
True
>>> action.run()
hi

Using bind:

>>> printLn("Hello").bind_(
... lambda: printLn("World")
... ).run()
Hello
World
classmethod fail(exception)

The failure of an IO monad is an exception

>>> IO.fail(KeyError("key"))
Traceback (most recent call last):
    ...
KeyError: 'key'
classmethod ret(value)

Returns a value in the IO monad:

>>> action = IO.ret("Hello")

You can then use bind that to an arrow:

>>> action.bind(printLn).run()
Hello

Base Monad classes

Use the following classes for defining your own monads.

class fp.monads.monad.Monad
classmethod ap(f, *monads, **kwarg_monads)

Monad.ap(f, *args, **kwargs) allows you to call a pure function within a Monad.

A normal function like f(x): x + 1 can be made to take a Maybe and return a Maybe without any boilerplating:

>>> from fp.monads.maybe import Maybe, Just, Nothing
>>> Maybe.ap(lambda x: x+1, Just(1))
Just(2)
>>> Maybe.ap(lambda x: x+1, Nothing)
Nothing

Here’s an example with an add function:

>>> import operator
>>> Maybe.ap(operator.add, Just(1), Just(2))
Just(3)
>>> Maybe.ap(operator.add, Nothing, Just(2))
Nothing
>>> Maybe.ap(operator.add, Just(1), Nothing)
Nothing

It even works with kwargs:

>>> from datetime import timedelta
>>> Maybe.ap(timedelta, days=Just(1), seconds=Just(60))
Just(datetime.timedelta(1, 60))
classmethod arrow_cl(arrow1, arrow2)

Left to Right arrow composition

This expressions means: maybe cast the string as an int, then maybe add 1

>>> from fp.monads.maybe import Maybe, Just
>>> from fp import p
>>> maybe_int = p(Maybe.catch, int)
>>> string_to_plus = Maybe.arrow_cl(maybe_int, lambda x: Just(x+1))
>>> string_to_plus("1")
Just(2)
>>> string_to_plus("a")
Nothing
classmethod arrow_cr(arrow1, arrow2)

Right to Left arrow composition

This expression means the same as: maybe cast the string as an int, then maybe add 1 but read right to left.

>>> from fp.monads.maybe import Maybe
>>> from fp import p
>>> maybe_int = p(Maybe.catch, int)
>>> string_to_plus = Maybe.arrow_cr(lambda x: Maybe(x+1), maybe_int)
>>> string_to_plus("1")
Just(2)
bind(f)

Pass the value of this monad into the action f

bind_(f)

Call the action f, throwing away the value of this monad

classmethod catch(f, *args, **kwargs)

Execute the function f(*args, **kwargs) and return the value inside the monad.

Catch any errors and call the monad’s fail method

>>> from fp.monads.maybe import Maybe
>>> from fp.monads.either import Either
>>> from fp.monads.iomonad import IO
>>> Maybe.catch(lambda: {'foo': 'bar'}['foo'])
Just('bar')
>>> Maybe.catch(lambda: {}['foo'])
Nothing
>>> Either.catch(lambda: {'foo': 'bar'}['foo'])
Right('bar')
>>> Either.catch(lambda: {}['foo'])
Left(KeyError('foo',))
>>> IO.catch(lambda: {'foo': 'bar'}['foo']).run()
'bar'
>>> IO.catch(lambda: {}['foo']).run()
Traceback (most recent call last):
    ...
KeyError: 'foo'
classmethod fail(error)

Return the failure case of the monad.

classmethod filterM(predM, items)
>>> from fp.monads.maybe import Maybe
>>> from fp import even, c, p
>>> maybeInt = p(Maybe.catch, int) # convert int to a Maybe arrow
>>> maybeEven = p(Maybe.ap, even) # lift even into Maybe
>>> Maybe.filterM(c(maybeEven, maybeInt), ["x","1","2","3","4"])
Just(['2', '4'])
classmethod mapM(arrow, items)

Map an arrow accross a list of values:

>>> from fp import p
>>> from fp.monads.maybe import Maybe
>>> maybe_int = p(Maybe.catch, int)
>>> Maybe.mapM(maybe_int, ["1", "2"])
Just([1, 2])
>>> Maybe.mapM(maybe_int, ["1", "a"])
Nothing
classmethod mapM_(arrow, items)
>>> from fp.monads.iomonad import IO, printLn
>>> action = IO.mapM_(printLn, ["Hello", "World"])
>>> action.run()
Hello
World
>>> action = IO.mapM_(printLn, ["Hello", "World"])
>>> action.run()
Hello
World
classmethod ret(value)

The return function for the monad

classmethod sequence(ms)

execute the monadic actions in ms, returning the result

>>> from fp.monads.maybe import Just, Nothing, Maybe
>>> Maybe.sequence([Just(1), Nothing, Just(2)])
Nothing
>>> Maybe.sequence([Just(1), Just(2)])
Just([1, 2])
classmethod sequence_(ms)

Execute the

>>> from fp.monads.iomonad import printLn, IO
>>> actions = [printLn("Hello"), printLn("World")]
>>> IO.sequence_(actions).run()
Hello
World
classmethod sequence_dict(d)

Perform all the actions inside a dictionary and return the result

>>> from fp.monads.maybe import Maybe, Just
>>> Maybe.sequence_dict({"foo": Just(1), "bar": Just(2)})  ==                Just({'foo': 1, 'bar': 2})
True
unless(b)

Execute the action when False, return a noop otherwise

>>> from fp.monads.iomonad import printLn
>>> _ = printLn("Hello").unless(False).run()
Hello
>>> _ = printLn("Hello").unless(True).run()
when(b)

Execute the action when True, return a noop otherwise

>>> from fp.monads.iomonad import printLn
>>> _ = printLn("Hello").when(True).run()
Hello
>>> _ = printLn("Hello").when(False).run()
class fp.monads.monad.MonadPlus

MonadPlus allows a Monad to define what a zero result is and a method for adding two MonadPlus instances together.

classmethod guard(b)
>>> from fp.monads.maybe import Maybe
>>> Maybe.guard(True).bind_(lambda: Maybe.ret("Hello"))
Just('Hello')
>>> Maybe.guard(False).bind_(lambda: Maybe.ret("Hello"))
Nothing
mfilter(pred)

Returns Monad.mzero if the pred is False.

>>> from fp.monads.maybe import Just
>>> from fp import even
>>> Just(4).mfilter(even)
Just(4)
>>> Just(3).mfilter(even)
Nothing
mplus(y)

An associative operation

classmethod msum(xs)

Reduces a list of MonadPlus instances using its mplus method:

>>> from fp.monads.maybe import Just, Nothing, Maybe
>>> Maybe.msum([Just(1), Nothing, Just(2)])
Just(1)
>>> Maybe.msum([Nothing, Nothing, Just(2)])
Just(2)
>>> Maybe.msum([Nothing, Nothing, Nothing])
Nothing

Project Versions

Previous topic

fp — A collection of functional programming inspired utilities

Next topic

fp.collections — Utilities for collections

This Page