PyMonad implements data structures typically available in pure functional or functional first programming languages like Haskell and F#. Included are Monad and Monoid data types with several common monads included - such as Maybe and State - as well as some useful tools such as the @curry decorator for defining curried functions. PyMonad 2.0.x represents and almost complete re-write of the library with a simpler, more consistent interface as well as type annotations to help ensure correct usage.
These instructions will get you a copy of the project up and running on your local machine for development and testing purposes.
PyMonad requires Python 3.7+. If installing via pip
then you
will also need Pip and Wheel installed. See those projects for
more information on installing them if necessary.
Potential contributors should additionally install pylint and pytype to ensure their code adheres to common style conventions.
From a command line run:
pip install PyMonad
Download the project files from https://pypi.org/project/PyMonad/#files and from the project directory run:
python setup.py install
If that doesn’t work you may need to run the following instead.
python3 setup.py install
Clone the project repository:
git clone https://github.com/jasondelaat/pymonad.git
Then from the project directory run setup.py
as for the manual
build instructions above.
The following example imports the tools
module and uses the
curry
function to define a curried addition function.
import pymonad.tools
@pymonad.tools.curry(2) # Pass the expected number of arguments to the curry function.
def add(x, y):
return x + y
# We can call add with all of it's arguments...
print(add(2, 3)) # Prints '5'
# ...or only some of them.
add2 = add(2) # Creates a new function expecting a single arguments
print(add2(3)) # Also prints '5'
The PyMonad documentation is a work in progress. For tutorials, how-to, and more head over to the PyMonad Documentation Project. If you’d like to contribute visit the documentation repository here.
If you’ve used the 1.x versions of PyMonad you’ll notice that there are a few differences:
Currying functions in PyMonad version 1.x wrapped a function in an instance of the Reader monad. This is no longer the case and currying simply produces a new function as one might expect.
The signature of curry
has changed slightly. The new curry
takes two arguments: the number of arguments which need to be
curried and the function.
from pymonad.tools import curry
def add(x, y):
return x + y
curried_add = curry(2, add)
# add = curry(2, add) # If you don't need access to the uncurried version.
curry
is itself a curried function so it can be used more
concisely as a decorator.
from pymonad.tools import curry
@curry(2)
def add(x, y):
return x + y
Version 2 of PyMonad discourages the use of operators (>>, \*, and
&) used in version 1 so old code which uses them will
break. Operators have been removed from the default monad
implementation but are still available for users that still wish
to use them in the operators
package. To use operators:
# Instead of this:
# import pymonad.maybe
# Do this:
import pymonad.operators.maybe
While it’s unlikely operators will be removed entirely, it is strongly suggested that users write code that doesn’t require them.
The fmap
method has been renamed to simply map
and unit
is now called insert
.
from pymonad.maybe import Maybe
def add2(x):
return x + 2
m = (Maybe.insert(1)
.map(add2)
)
print(m) # Just 3
Previously applicative syntax used the &
operator or the amap
method. amap
still exists but there’s now another way to use
applicatives: apply().to_arguments()
from pymonad.tools import curry
from pymonad.maybe import Maybe, Just
@curry(2)
def add(x, y):
return x + y
a = Just(1)
b = Just(2)
c = Maybe.apply(add).to_arguments(a, b)
print(c) # Just 3
If the function passed to apply
accepts multiple arguments then
it must be a curried function.
The then
method combines the functionality of both map
and
bind
. It first tries to bind
the function passed to it and,
if that doesn’t work, tries map
instead. It will be slightly
less efficient than using map
and bind
directly but frees
users from having to worry about specifically which functions are
being used where.
from pymonad.tools import curry
from pymonad.maybe import Maybe, Just, Nothing
@curry(2)
def add(x, y):
return x + y
@curry(2)
def div(y, x):
if y == 0:
return Nothing
else:
return Just(x / y)
m = (Maybe.insert(2)
.then(add(2)) # Uses map
.then(div(4)) # Uses bind
)
print(m) # Just 1.0
Previously, if you need to get a value out of a Maybe
or an
Either
after a series of calculations you would have to access
the .value
property directly. By the very nature of these two
monads, .value
may not contain valid data and checking whether
the data is valid or not is the problem these monads are supposed
to solve. As of PyMonad 2.3.0 there are methods – maybe
and
either
– for properly extracting values from these
monads.
Given a Maybe
value m
, the maybe
method takes a default
value, which will be returned if m
is Nothing
, and a function
which will be applied to the value inside of a Just
.
from pymonad.maybe import Just, Nothing
a = Just(2)
b = Nothing
print(a.maybe(0, lambda x: x)) # 2
print(b.maybe(0, lambda x: x)) # 0
The either
method works essentially the same way but takes two
functions as arguments. The first is applied if the value is a
Left
value and the second if it’s a Right
.
from pymonad.either import Left, Right
a = Right(2)
b = Left('Invalid')
print(a.either(lambda x: f'Sorry, {x}', lambda x: x)) # 2
print(b.either(lambda x: f'Sorry, {x}', lambda x: x)) # Sorry, Invalid
In pymonad versions 2.3.4 and earlier, an error in the
implementation of then
, detailed here, meant that some monad
types executed then
with exponential complexity. As of version
2.3.5 this has been corrected. All monad types now execute then
in linear time. A similar problem occured with the map
and
bind
methods for the State monad which have also been fixed in
2.3.5
If you’re using an earlier version of pymonad upgrading to 2.3.5 is highly recommended.
These tests primarily ensure that the defined monads and monoids obey the required mathematical laws.
On most *nix systems you should be able to run the automated tests by typing the following at the command line.
./run_tests.sh
However, run_tests.sh
is just a convenience. If the above doesn’t
work the following should:
python3 -m unittest discover test/
Contributors only need to run pylint
and pytype
over their
code and ensure that there are no glaring style or type
errors. PyMonad (mostly) attempts to adhere to the Google Python Style Guide
and includes type hinting according to PEP 484.
In general, don’t disable pylint
or pytype
errors for the
whole project, instead disable them via comments in the code. See
the existing code for examples of errors which can be disabled.
Jason DeLaat - Primary Author/Maintainer - https://github.com/jasondelaat/pymonad
This project is licensed under the 3-Clause BSD License. See LICENSE.rst for details.