Pycategories: Functors, Monads, and more

master pipeline master coverage

Pycategories is a Python 3 library that implements ideas from category theory, such as monoids, functors, and monads. It provides a Haskell-influenced interface for defining instances of those typeclasses and defines several right out of the box, for example the Maybe monad:

>>> from categories import apply
>>> from categories.maybe import Just, Nothing
>>> f = Just(lambda x: x ** 2)
>>> x = Just(17)
>>> apply(f, x)
Just(289)
>>> apply(f, Nothing())
Nothing

Or to define your own instance of a typeclass:

>>> from categories import mappend, mempty, monoid
>>> monoid.instance(dict, lambda: {}, lambda a, b: dict(**a, **b))
>>> mappend({'foo': 'bar'}, {'rhu': 'barb'})
{'foo': 'bar', 'rhu': 'barb'}

Installation

pip install pycategories

To clone the repo and install dependencies for development:

git clone https://gitlab.com/danielhones/pycategories
cd pycategories
pip install -e .[dev]

License

Pycategories is licensed under the MIT License

User Guide

Introduction

This library takes its inspiration from category theory and Haskell. Its goal is to provide a useful set of features that enable programming in a functional style in Python, with category theory as the guiding principles. If you are new to category theory or don’t know Haskell, Category Theory for Programmers by Bartosz Milewski is a great textbook that is immediately useful as a programmer and it covers the basics of Haskell and Category Theory. It also has two sets of video lectures to accompany the text.

The main features of this library are provided by typeclasses and some data types that defines instances of those typeclasses. A typeclass is like a declaration of an interface for an object. Their use in this library is to define interfaces for algebraic structures that come from category theory. There are certain laws that each of those algebraic structures should obey, and the module for each typeclass defines functions that allow you to check whether a data type conforms to those laws. For examples on how to use those functions, take a look at the tests for Maybe or Python builtins in the Pycategories source code.

These are the typeclasses defined in Pycategories:

These are the data types provided by Pycategories, with the specified typeclass instances defined:

  • Maybe
    • Semigroup
    • Monoid
    • Functor
    • Applicative
  • Either
    • Functor
    • Applicative
    • Monad
  • Validation
    • Semigroup
    • Functor
    • Applicative

And typeclass instances are defined for the following built-in Python types:

  • List
    • Semigroup
    • Monoid
    • Functor
    • Applicative
    • Monad
  • String
    • Semigroup
    • Monoid
    • Functor
  • Tuple
    • Semigroup
    • Monoid
    • Functor
    • Applicative
    • Monad

The behavior of the instances defined for Functor, Applicative, and Monad match the behavior of those definitions in Haskell. If you need something else for your application, you can redefine the instances for tuple, or define a new product data type that’s isomorphic to a tuple.

Quickstart

Basics

You can get a lot done by just using the data types provided by Pycategories and the instances defined for some of Python’s built-in types. For example, the following example script uses Maybe to convert the contents of a file to uppercase and print the result, or exit with no output if the file doesn’t exist. This could be altered to use Either to display an error message instead of exiting with no output for non-existent files:

from functools import partial
import os
import sys

from categories import fmap
from categories.maybe import Just, Nothing
from categories.utils import compose


def maybe_read_file(path):
    if os.path.exists(path):
        with open(path, 'r') as f:
            return Just(f.read())
    else:
        return Nothing()


def upper_case_file(path):
    return fmap(lambda x: x.upper(),
                maybe_read_file(path))


if __name__ == '__main__':
    maybe_upper_case = compose(partial(fmap, print),
                               upper_case_file)
    maybe_upper_case(sys.argv[1])

Each data type defines a new class to represent the type, and one or more data constructors that create and return objects of that type. For example, the Maybe data type has two data constructors, Just and Nothing. To create a Maybe object, you would use one of those constructors:

>>> Just(23)
Just(23)
>>> Nothing()
Nothing

Data constructors are used just like Python functions. If a data constructor takes no arguments, then you still need a set of parentheses after it, unlike in Haskell. Even though the data constructor names are capitalized, like Python class names conventionally are, they are not currently implemented as separate classes, and are actually functions that return objects of the data type class. This should be considered an implementation detail of the library and you should not rely on that behavior in your application code, since it may change in the future.

There is basic pattern matching available via the match() function on the data types provided by Pycategories. This allows you to avoid doing manual checking of attributes of a data type. Currently, the match function only matches on the data constructor, and not on value. It takes a data constructor as an argument and returns a boolean indicating whether the object it was called on matches that constructor. For example, reusing the maybe_read_file function in the previous example:

data = maybe_read_file('/tmp/testfile.txt')

if data.match(Just):
    print("The file existed and contained:\n\n{}".format(data.value))
else:
    print("That file did not exist")

However, it is more common in functional programming to take the opposite approach and lift your normal functions into the context of a datatype using bind, apply, or fmap as appropriate, like the first example script does with fmap.

Defining Instances

A primary use of this library is defining typeclass instances for your own data types. The Python module that defines each typeclass will have a function called instance, which lets you define your own instances. As an example, let’s define monoid and functor instances for Python’s built-in dictionary:

>>> from categories import monoid, functor
>>> monoid.instance(dict, lambda: {}, lambda a, b: dict(**a, **b))
>>> functor.instance(dict, lambda f, xs: {k: f(v) for k, v in xs.items()})

Now that we’ve done that, we can call mappend and fmap on a dictionary:

>>> from categories import fmap, mappend
>>> test = mappend({'x': 'foobar'}, {'y': 'bazquux'})
{'x': 'foobar', 'y': 'bazquux'}
>>> fmap(lambda x: x.upper(), test)
{'x': 'FOOBAR', 'y': 'BAZQUUX'}

To be truly useful, typeclass instances should obey certain laws that make them behave consistently. To help test whether they conform to those laws, the typeclass modules have functions that return a boolean indicating whether the instance obeys the law. To use them, you need to call them with some example values of the type you’re testing; the specific arguments needed can be different for each law function. The files in the tests/ directory have lots of examples of using those functions. Here’s a brief example testing the monoid and functor laws for dictionary:

>>> monoid.identity_law({'a': 'test', 'b': 'other'})
True
>>> monoid.associativity_law({'a': 'test'}, {'b': 'other'}, {'c': 'something else'})
True
>>> functor.identity_law({'a': 'test', 'b': 'other'})
True
>>> f = lambda x: x.upper()
>>> g = lambda y: y[0:3]
>>> functor.composition_law(f, g, {'x': 'foobar', 'y': 'bazquux'})
True

For details on what each typeclass needs when defining instances, refer to the API documentation.

API

Typeclasses

categories.semigroup.instance(type, sappend)[source]
categories.monoid.instance(type, mempty, mappend)[source]
categories.functor.instance(type, fmap)[source]
categories.applicative.instance(type, pure, apply)[source]
categories.monad.instance(type, mreturn, bind)[source]

Data Types

class categories.maybe.Maybe(type: str, value: A = None)[source]
class categories.either.Either(type: str, value: Union[E, A])[source]
class categories.validation.Validation(type: str, value: Union[E, A])[source]

Utilities

categories.utils.compose(*fs) → Callable[source]

Return a function that is the composition of the functions in fs. All functions in fs must take a single argument.

Adapted from this StackOverflow answer: https://stackoverflow.com/a/34713317

Example:
>>> compose(f, g)(x) == f(g(x))
categories.utils.flip(f: Callable[[A, B], Any]) → Callable[[B, A], Any][source]

Return a function that reverses the arguments it’s called with.

Parameters:

f – A function that takes exactly two arguments

Example:
>>> exp = lambda x, y: x ** y
>>> flip_exp = flip(exp)
>>> exp(2, 3)
8
>>> flip_exp(2, 3)
9
categories.utils.unit(x: Any) → Any

The identity function. Returns whatever argument it’s called with.

Contents

categories package
Submodules
categories.applicative module
class categories.applicative.Applicative(pure, apply)[source]

Bases: object

categories.applicative.composition_law(u, v, w, type_form=None)[source]
pure (.) <*> u <*> v <*> w == u <*> (v <*> w)
where (<*>) = apply
categories.applicative.homomorphism_law(f, x, _type, type_form=None)[source]
pure f <*> pure x == pure (f x)
where (<*>) = apply
categories.applicative.identity_law(v, type_form=None)[source]
pure id <*> v == v
where (<*>) = apply
categories.applicative.instance(type, pure, apply)[source]
categories.applicative.interchange_law(u, y, type_form=None)[source]
u <*> pure y == pure ($ y) <*> u
where (<*>) = apply
categories.applicative.pure(x, _type, type_form=None)[source]

The optional type_form arg is an attempt to provide some more flexibility with the type that pure is required to cast to. For example, pure for the tuple Applicative instance needs to know the types of its elements so it can return the correct object:

pure(7, tuple, type_form=(str, int)) == (“”, 7)

categories.builtins module
categories.either module
class categories.either.Either(type: str, value: Union[E, A])[source]

Bases: typing.Generic

classmethod left(value: E) → Etr[source]
match(constructor) → bool[source]
classmethod right(value: A) → Etr[source]
categories.either.either(f: Callable, g: Callable, x: categories.either.Either) → categories.either.Either[source]

Given two functions and an Either object, call the first function on the value in the Either if it’s a Left value, otherwise call the second function on the value in the Either. Return the result of the function that’s called.

Parameters:
  • f – a function that accepts the type packed in x
  • g – a function that accepts the type packed in x
  • x – an Either object
Returns:

Whatever the functions f or g return

categories.functor module
class categories.functor.Functor(fmap)[source]

Bases: object

categories.functor.composition_law(f, g, x)[source]

fmap (g . f) == fmap g . fmap f

categories.functor.identity_law(x)[source]

fmap id == id

categories.functor.instance(type, fmap)[source]
categories.instances module
class categories.instances.Typeclass[source]

Bases: object

categories.instances.make_adder(instances: Dict[Type[CT_co], categories.instances.Typeclass]) → Callable[[Type[CT_co], categories.instances.Typeclass], None][source]
categories.instances.make_getter(instances: Dict[Type[CT_co], categories.instances.Typeclass], name: str) → Callable[[Type[CT_co]], categories.instances.Typeclass][source]
categories.instances.make_undefiner(instances: Dict[Type[CT_co], categories.instances.Typeclass]) → Callable[[Type[CT_co]], None][source]
categories.maybe module
class categories.maybe.Maybe(type: str, value: A = None)[source]

Bases: typing.Generic

classmethod just(value: A) → M[source]
match(constructor) → bool[source]
classmethod nothing() → M[source]
categories.maybe.maybe(default: B, f: Callable, x: categories.maybe.Maybe[~A][A]) → B[source]

Given a default value, a function, and a Maybe object, return the default if the Maybe object is Nothing, otherwise call the function with the value in the Maybe object, call the function on it, and return the result.

Parameters:
  • default – This is the value that gets returned if x is Nothing
  • f – When x matches Just, this function is called on the value in x, and the result is returned
  • x – a Maybe object
Returns:

Whatever type default or the return type of f is

categories.monad module
class categories.monad.Monad(bind, mreturn)[source]

Bases: object

categories.monad.associativity_law(m, f, g, type_form=None)[source]

(m >>= f) >>= g == m >>= (x -> f x >>= g)

categories.monad.instance(type, mreturn, bind)[source]
categories.monad.left_identity_law(a, f, _type, type_form=None)[source]

mreturn a >>= f == f a

categories.monad.mreturn(x, _type, type_form=None)[source]
categories.monad.right_identity_law(m, type_form=None)[source]

m >>= return == m

Ex:
Just 7 >>= return == Just 7
categories.monoid module
class categories.monoid.Monoid(mempty, mappend)[source]

Bases: object

categories.monoid.associativity_law(x, y, z)[source]
(x <> y) <> z = x <> (y <> z)
where (<>) = mappend
categories.monoid.identity_law(x)[source]

Assert left and right identity laws:

mempty <> x = x x <> mempty = x

where (<>) = mappend
categories.monoid.instance(type, mempty, mappend)[source]
categories.monoid.mempty(type)[source]
categories.utils module
categories.utils.flip(f: Callable[[A, B], Any]) → Callable[[B, A], Any][source]

Return a function that reverses the arguments it’s called with.

Parameters:

f – A function that takes exactly two arguments

Example:
>>> exp = lambda x, y: x ** y
>>> flip_exp = flip(exp)
>>> exp(2, 3)
8
>>> flip_exp(2, 3)
9
categories.utils.funcall(f: Callable, *args) → Any[source]
categories.utils.id_(x: Any) → Any[source]

The identity function. Returns whatever argument it’s called with.

categories.utils.unit(x: Any) → Any

The identity function. Returns whatever argument it’s called with.

Module contents

Indices and tables