Overview

Installing

HaskPy can be installed, for instance, from PyPI:

pip install haskpy

Development version is available at GitHub.

The main contribution of HaskPy is to provide powerful types and typeclasses inspired by Haskell. This section gives just a quick glimpse on some simple usage, so refer to the API documentation for more detailed explanations.

If you just would like to know:

Make duck typing much more powerful by using sound and powerful interfaces.

Make Python more pythonic with powerful base classes.

This means that we can write code that is more generic and polymorphic.

To get started, import HaskPy:

>>> import haskpy as hp

Types

For a full list of available types, refer to haskpy.types module. This section highlights a few of the most important ones.

Maybe

It is very common in programming to handle variables that might have a value or not. In Python, this is typically done by using None to represent “no value”. With HaskPy, this can be represented by Maybe type with Just() constructor for values and Nothing for “no value”. This way you don’t need to write if-else but you can treat the object similarly regardless of whether it actually contains a value or not:

>>> inc = lambda v: v + 1
>>> x = hp.Just(42)
>>> x.map(inc)
Just(43)
>>> y = hp.Nothing
>>> y.map(inc)
Nothing

Maybe also helps you to avoid a common mistake that in some part of the code you forget to take into account that a variable can be None too. See maybe for more details and examples.

Functions

HaskPy provides decorator function() to make functions more powerful. The decorator converts the function into Function object which has a lot more features than just being callable. For instance, it’s a monad. In addition, the function is curried in such a way that it can be called with any number of arguments and it stays partially applied until all arguments have been passed:

>>> f = hp.function(lambda x, y, z: x + y + z)
>>> f("a")("b", "c")
"abc"

Many of the methods (i.e., functions bound to objects) in HaskPy have corresponding function counterparts. This is sometimes useful because functions are easier to pass as arguments and only the functions have been decorated with function(). For instance, the example above could have been written with map() function:

>>> hp.map(inc, x)
Just(43)
>>> hp.map(inc, y)
Nothing

To apply a two-argument function to values that are both inside Maybe structure, one can use lift2():

>>> add = hp.function(lambda a, b: a + b)
>>> hp.lift2(add, x, x)
Just(84)
>>> hp.lift2(add, x, y)
Nothing

See Function for more details and examples on HaskPy function type. See utils for a small collection of simple functions. Most functions in the entire HaskPy package are imported so that they are available directly under haskpy package as shown above with hp.map and hp.lift2.

List

List is another basic type in HaskPy. Lists can be, for instance, mapped:

>>> xs = hp.List(10, 20, 30)
>>> hp.map(inc, xs)
List(11, 21, 31)

and concatenated:

>>> ys = hp.List(40, 50)
>>> hp.append(xs, ys)
List(10, 20, 30, 40, 50)

For more details, see list.

Todo

Dictionary

Type signature

Typeclasses

For a full list of typeclasses, see haskpy.typeclasses. This section highlights a few of the most important ones.

Functors, applicatives and monads

I recommend that you read “Functors, Applicatives, and Monads in Pictures” for a good illustrated explanation about the concepts if you’re not familiar with them.

In short, Functor represents some kind of structure (think of it as a container) that can contain values of some type in such a way that those values can be modified without modifying the structure/container. This modification of the values is called functorial mapping and map() does that. You’ve already seen a few examples above on how to use it for Maybe and List, and also functions are functors. Functor is an extremely common structure in programming. You can also lift over a nested structure by using multiple maps:

>>> hp.map(hp.map(inc))(hp.List(hp.Just(42), hp.Nothing, hp.Just(100)))
List(Just(43), Nothing, Just(101))

Applicative is a special case of Functor. Applicative types know how to apply functions to values when both the functions and the values are wrapped inside structure. This is done with apply() function:

>>> inc_maybe = hp.Just(inc)
>>> hp.apply(inc_maybe, hp.Just(42))
Just(43)
>>> hp.apply(hp.Nothing, hp.Just(42))
Nothing
>>> square = hp.function(lambda v: v**2)
>>> fs = hp.List(inc, square)
>>> hp.apply(fs, hp.List(1, 10, 100))
List(2, 11, 101, 1, 100, 10000)

Notice how with lists each function is applied to each value in the other list. One common usecase for applicatives is a function that is applied to multiple arguments that are all inside structure: map over the first argument, then the partially applied function gets inside the structure, so all the remaining arguments are applied with the applicative apply:

>>> add3 = hp.function(lambda a, b, c: a + b + c)
>>> add3_a = hp.map(add3, hp.Just(42))
>>> add3_ab = hp.apply(add3_a , hp.Just(100))
>>> hp.apply(add3_ab, hp.Just(1))
Just(143)

Or just lift3() for short:

>>> hp.lift3(add3, hp.Just(42), hp.Just(100), hp.Just(1))
Just(143)

Finally, Monad is a special case of Applicative. Monads support yet another slightly different way of applying a function: the argument is again inside a structure but the function itself isn’t, only its output. Alright, that might be a bit difficult to understand. Let’s start from the function. The following function inverts the given number, but it handles zeros explicitly by using Maybe:

>>> invert = hp.function(lambda x: hp.Nothing if x == 0 else hp.Just(1/x))
>>> invert(4)
Just(0.25)
>>> invert(0)
Nothing

Given this function and a value that is also wrapped inside the same structure, we can use bind() function:

>>> hp.bind(hp.Just(10), invert)
Just(0.1)
>>> hp.bind(hp.Just(0), invert)
Nothing
>>> hp.bind(hp.Nothing, invert)
Nothing

For more details, see functor, apply_, bind_, applicative and monad documentation. There are many other useful monads mentioned in a section bit below.

Monoids

Monoid is a typeclass for types whose values can be “merged”. That is, there’s a binary operator that takes two arguments of the same type and returns a result of that type.

>>> xs = hp.List(10, 20, 30)
>>> ys = hp.List(40, 50)
>>> hp.append(xs, ys)
List(10, 20, 30, 40, 50)
>>> s1 = hp.String("foo")
>>> s2 = hp.String("bar")
>>> hp.append(s1, s2)
String('foobar')
>>> u = hp.Just(hp.List(10, 20))
>>> v = hp.Just(hp.List(30))
>>> hp.append(u, v)
Just(List(10, 20, 30))

Each monoid also has a class attribute called Monoid.empty which is an identity element of the binary operation. That is, combining value X with the identity element gives X as the result:

>>> hp.append(xs, hp.List.empty)
List(10, 20, 30)
>>> hp.append(hp.String.empty, s2)
String('bar')
>>> hp.append(u, hp.Just.empty)
Just(List(10, 20))

For more details, see monoid and semigroup. There are also a few simple monoids implemented in monoids such as All.

Foldables

Foldable is a typeclass for structure that can be “squashed”, or folded. It means that all the values in the structure can be processed into a single value and the container structure disappears. The way that the values are processed one by one can be controlled by the choice of the folding function (e.g., foldr(), foldl(), fold_map()).

For instance, to calculate the sum of the elements in a list:

>>> hp.foldl(lambda a: lambda b: a + b, 0, hp.List(1, 2, 3, 4))
10

If the values in the container are of monoid type, you can use fold():

>>> hp.fold(hp.All, hp.List(hp.All(True), hp.All(True), hp.All(False)))
All(False)

For more information, see foldable. If you’re interested in how to fold infinite lists by short-circuiting and support tail-call optimization, see foldr_lazy().

Traversables

Todo

Traversable. Show how to flip the structures.

Monad examples

Todo

State, Reader, Writer

Operators

Operators sometimes provide much nicer user experience than using functions. However, Python has a fixed set of operators and one cannot define their own operators, so one can use only the predefined ones. But using those operators for a completely different purpose might be very confusing for the user as the operators suddenly mean something totally different - or not, depending on the context. Despite this major drawback, HaskPy defines new interpretations for a few operators just to make it possible to write more compact code. However, it’s up to the user to decide whether it makes sense to use them or not.

Mapping can be done with **:

>>> xs = hp.List(10, 20, 30)
>>> inc = lambda x: x + 1
>>> inc ** xs
List(11, 21, 31)

Applicative applying is done with @:

>>> ys = hp.List(1, 2)
>>> add = lambda x: lambda y: x + y
>>> add ** xs @ ys
List(11, 12, 21, 22, 31, 32)

Applicative sequencing is done with >>:

>>> xs >> ys
List(1, 2, 1, 2, 1, 2)

Monadic binding is done with %:

>>> hp.Just(10) % invert
Just(0.1)

Monoid values are combined with +:

>>> xs + ys
List(10, 20, 30, 1, 2)

For more details about why these operators were chosen, see the documentation of the operators Functor.__rpow__(), Applicative.__matmul__(), Applicative.__rshift__(), Bind.__mod__() and Semigroup.__add__().

Algebraic data types (ADTs)

Algebraic data types (ADTs) are composite types constructed by using product and sum types. A product type means that the values of that type contain values of all the types in the product. A sum type means that the values of that type contain a value of one of the types in the sum.

There are many ways to achieve this in Python, but in HaskPy the following approach has been used in Maybe and Either: Define the ADT as a class which has match method given as an argument when constructing values. The sum type is represented by different function that create those objects and the product type is represented by the arguments to those functions.

For instance, MyMaybe a = MyJust a | MyNothing can be represented as:

class MyMaybe():

    def __init__(self, match):
        self.match = match

def MyJust(x):
    return MyMaybe(lambda *, MyJust, MyNothing: MyJust(x))

MyNothing = MyMaybe(lambda *, MyJust, MyNothing: MyNothing())

The purpose of match method is simple pattern matching:

>>> inc_or_die = hp.match(
...     MyJust=lambda x: x + 1,
...     MyNothing=lambda: 666
... )
>>> x = MyJust(42)
>>> inc_or_die(x)
43
>>> y = MyNothing
>>> inc_or_die(y)
666

Note that with this kind of pattern matching, one is forced to provide a function to handle each case so the pattern matching is total (no missed cases).

This definitely isn’t the only way to construct ADTs in Python. A natural alternative would be to use subclasses. With the approach shown above, it is clear that MyJust and MyNothing are just data constructors, they cannot add any methods of their own. That is MyJust and MyNothing have exactly the same interface. This isn’t forced when using subclasses where one could define a method for MyNothing only. Also, the implementations of the methods are split into multiple classes whereas with this approach all methods are implemented in MyMaybe class and it’s easy to see what the method does in each case (see, for instance, the source of Maybe). With inheritance, one might easily forget to implement some required methods in some of the subclasses.

Profunctor optics

Compose and monad transformers

monad transformers

Recursion

recursion with tco and short-circuiting

Property-based testing