Applicatives, or functors for arbitrary functions

Mapping only single param functions can be limiting. It would be cool if lift would could work on function with arbitrary many parameters.

But before we begin with our attempts at solution, we need to define our fmap and lift once again:

In [4]:
from functools import singledispatch, partial

def fmap(x, fn):
    raise Error("Not implemented for" + a)
def lift(fn):
    def liftedfn(x):
        return fmap(x, fn)
    return liftedfn 

Now we could register a few classes that would work with this. I.e. all of the collections such as list:

In [5]:
def _(lst, fn):
    return [fn(x) for x in lst]

Could currying rescue us?

Currying is a concept, where instead of passing in all the params into the function at once, you pass in the first param and grt a new function as a result. This function accepts the second param and returns a function that accepts the third, and by now you should be getting a picture.

Or maybe I should show an example.Lets invent a nontrivial 3 arg function, we will call it erm, window (because slice was taken).

In [6]:
from functools import partial

def window(begin,end,array):
    return array[begin:end]

[3, 4]

If this function were curried, it would look like this:

In [7]:
def curried_window(begin):
    return lambda end: (lambda array: array[begin:end])
[3, 4]

Some functional languages have this as a default. The nice thing about that is, that you can quickly create more specific fuctions, without the need to wrap them in new function definitions.

And this seems to solve out problem with multi param function, because sudenly, everything is a single param function, once you curry :)

In [9]:
[<function __main__.curried_window.<locals>.<lambda>>,
 <function __main__.curried_window.<locals>.<lambda>>,
 <function __main__.curried_window.<locals>.<lambda>>,
 <function __main__.curried_window.<locals>.<lambda>>]

Ok, as you can see, currying won't save us just yet, because even though we can use map with them, result is kind-of useless list of functions. We would need to somehow be able to apply this list of functions on a list of values.

In [10]:
def apply(lx, lfn):
    raise Error("Not implemented for" + lx)

def apply(lx,lfn):
    return [fn(x) for fn in lfn for x in lx]

a0 = map(curried_window,[1,2])
a1 = apply([5,6],a0)
a2 = apply([

[[2, 3, 4, 5],
 [2, 3, 4, 5],
 [2, 3, 4, 5, 6],
 [2, 3, 4, 5, 6],
 [3, 4, 5],
 [3, 4, 5],
 [3, 4, 5, 6],
 [3, 4, 5, 6]]

Yay, this seems to have worked :)

Now we can try to combine currying with apply and create a propper lift function :)

In [13]:
def curry(n, fn):
    if n == 1:
        return fn
    if n == 2:
        return lambda x:partial(fn,x)
        return lambda x:curry(n-1,partial(fn,x))
def lift(fn):
    def lifted(arg0, *args):
        result_fn = map(curry(len(args)+1, fn),arg0)
        for a in args:
            result_fn = apply(a, result_fn)
        return result_fn
    return lifted

There is one more assumption we made, when creating this lift function. On the line where we state:

result = map(curry(len(args)+1, fn),arg0)

we assume that you can use map function on the data-type we are lifting.

In [14]:
liftedw = lift(window)
liftedw([1,2], [5,6], [
[[2, 3, 4, 5],
 [2, 3, 4, 5],
 [2, 3, 4, 5, 6],
 [2, 3, 4, 5, 6],
 [3, 4, 5],
 [3, 4, 5],
 [3, 4, 5, 6],
 [3, 4, 5, 6]]

So, why to through all of this trouble to get the equivalent of i.e:

In [15]:
 for x in [1,2] 
 for y in [5,6] 
 for z in [[1,2,3,4,5,6],[1,2,3,4,5,6]]]
[[2, 3, 4, 5],
 [2, 3, 4, 5],
 [2, 3, 4, 5, 6],
 [2, 3, 4, 5, 6],
 [3, 4, 5],
 [3, 4, 5],
 [3, 4, 5, 6],
 [3, 4, 5, 6]]

I have two reasons:

  • We could use this pattern for things that are not iterable :)
  • we can analyze the pattern in isolation and figure out interesting relations

Lifting values

So far, we were concerned about lifting functions, but we would probably like to be able to lift arbitrary values as well. As you can see in our lifted definition, we have so far assumed, that we are lifting at least an unary function. Because we can't derive the wrapper for values from map and apply, we need to define it for the specific datatype. We uaually call this function pure

In [11]:
def pure(value):
    return [value]

Interesting thing with pure is, that with it defined, we should be able to replace uses of map(fn,o) with apply(pure(fn),o).

Algebra behind the applicative

So far we have not been explicit about our formal requirements on map, applicative and pure. I have mentioned that map should behave sensible and that there is some relationship between functors, lifting and category theory.

In my mind, the most sensible thing, is to think about functor as a lawful interface. Lawful interface would mean, that it conforms to more requirements, than just the ability to i.e. perform a method call.

A type T describes a functor if:

  • we can consider T a wrapper over arbitrary type A
  • there is a function map, that
    • accepts function f, that accepts a and returns b
    • accepts T wrapping a
    • returns T wrapping b

In haskell we would say that map has type (a -> b) -> T a -> T b With Python's typing library we could say that the type of map is

Callable[[Callable[[A],B], T[A]],T[B]]

The map function then needs to conform to these laws

  • for any t, map(lambda x:x ,t) == t
  • for any t and any two composable functions f and g, map(f,map(g,t)) == map(lambda x: f(g(x)),t)

If we define id and compose functions:

def id(x):
  return x

def compose(f,g):
  def composed(x)
    return f(g(x))
  return composed

we can swap the lambda expressions in the laws:

  • for any t, map(id ,t) == t
  • for any t and any two composable functions f and g, map(f,map(g,t)) == map(compose(f,g),t)

For applicative, we need it to implement be functor as well as

  • have function pure that accepts arbitrary a and returnt T a
  • have function apply, that
    • for any function f, that accepts a and returns b
    • accepts T wrapping f
    • accepts T wrapping a
    • returns T wrapping b

In haskell, we would say, that pure has type a -> T a and apply T (a -> b) -> T a -> T b With Python's typing library we could say that the type of pure is

Callable[[A], T[A]]

And type of apply is

Callable[[T[Callable[[A],B]], T[A]],T[B]]

The apply and pure functions need to conform to these laws:

  • for any t, apply(pure(id),t) == t
  • for any function f and type x, apply(pure(f),pure(x)) == f(x)
  • for any t, x, apply(t, pure(x)) == apply(pure(lambda g: g(x)),t)
  • for any u,v,w, apply(apply(apply(pure(compose),u),v),w) == apply(u,apply(v,w))
    • be aware that apply is not required to be associative, i.e. you can't state what ?? should be in apply(apply(u,v),w) ?? apply(u,apply(v,w))

Parametrized lift

For some of the following examples, I need to be able to lift into different domains. I might want to do this by making the lift function parametrized. We can't unfortunately do this with single dispatch, because the pure function would have nothing to dispatch on. This is similiar to our problem with empty when dealing with monoids.

This is why we will make applicative a class

In [36]:
class Applicative:
    def pure(self, val):
        raise NotImplementedError();
    def apply(self, fn, val):
        raise NotImplementedError();
    def lift(self, fn):
        def lifted(arg0, *args):
            result = self.apply(self.pure(curry(len(args)+1, fn)),arg0)
            for a in args:
                result = self.apply(result, a)
            return result
        return lifted

Because applicatives are not native to python, I am not entirely sure how to best define them. But this way looks simple enough.

Solving the null-pointer exception?

One thing we could try, is to solve the NPE problem with this. If you think about it, when you have function, that doesn't expect None, but gets it, only reasonable thing we could do with this, is return None.

We can define our wrapper function pure to just return the value back. We wouldn't be able to do this kind of cheating in haskell, where we would need to explicitly box the type. In Python this seems to be working.

We define our apply function to return None if any of the params are None. We can try this on int function.

In [18]:
class ApplyNone(Applicative):
    def pure(self, val):
        return val

    def apply(self, lfn,lx):
        if lfn!=None and lx!=None:
            return lfn(lx)
        return None

nint = ApplyNone().lift(int)

Now we can compare:

In [19]:
TypeError                                 Traceback (most recent call last)
<ipython-input-19-0030a6dbdf17> in <module>()
      1 print(int("1"))
----> 2 print(int(None))

TypeError: int() argument must be a string, a bytes-like object or a number, not 'NoneType'
In [20]:

To be honest, this is sort of a toy example. You probably don't want to just wrap your functions with this and instead of error at the place of failure just recieve the propagated nulls everywhere. But we can do better.

Applicative Validation

So what about some other uses? One I really like is using applicatives for validation.

First, let me harp on a thing I don't like about exceptions. Once you throw it, you are aborting the execution. I especially dislike it, if somebody uses exceptions to validate function params, i.e.:

In [21]:
def validated_window(begin, end, arr):
    if begin < 0:
        raise Exception("Begin was negative")
    if end < 1:
        raise Exception("End was not positive")
    return window(begin,end,arr)

Exception                                 Traceback (most recent call last)
<ipython-input-21-7d3436bc90f5> in <module>()
      6     return window(begin,end,arr)
----> 8 validated_window(-1,-1,[])

<ipython-input-21-7d3436bc90f5> in validated_window(begin, end, arr)
      1 def validated_window(begin, end, arr):
      2     if begin < 0:
----> 3         raise Exception("Begin was negative")
      4     if end < 1:
      5         raise Exception("End was not positive")

Exception: Begin was negative

Exception only told me about the frst problem. But with applicative pattern, I can rewrite it in such way, that I would get all of them.

I will define two validating functions:

In [22]:
def valid_begin(begin):
    return begin if begin >= 0 else Exception("Begin was negative")

def valid_end(end):
    return end if end > 0 else Exception("End was not positive")

Now, let me define how would I like to write the validated function.

In [23]:
def validated_window(begin, end, arr):    
    return liftedw(valid_begin(begin),valid_end(end),arr)

Now the interesting part of the puzzle is to redefine applyy once again, from thing that processes values in arrays, to thing that can deal with values that might be useless Exceptions.

In [24]:
class ApplyError(Applicative):
    def pure(self, val):
        return val

    def apply(self,lfn,lx):
        if isinstance(lfn,Exception) and isinstance(lx,Exception):
            return Exception(*lfn.args,*lx.args)
        if isinstance(lfn,Exception):
            return lfn    
        if isinstance(lx,Exception):
            return lx
        return lfn(lx)

As you can see, there are 3 cases to consider:

  • if both fn and x contain exceptions, I return agregate exception
  • if fn or x contain exceptions, I return the exception
  • if none of these happen, I can return fn(x)
In [25]:
liftedw = ApplyError().lift(window);
Exception('Begin was negative', 'End was not positive')

This style of applicative validation is more or less taken from the Purescript book. There is a difference, in the explicit boxing and unboxing I have mentioned earlier. If we explixitly boxed i.e. the Exception in an object of i.e. Validated class, we might be able to use some nice features of python, like sinlge dispatch. On the other hand, the lack of static type checking means, that the boxing wouldn't really help us.

Applicative generators

In python we have the concept of generator, that just produces a stream values. For example, we could have a infinite stream of positive and negative numbers.

In [27]:
def positive():
    i = 0;
    while True:
        yield i

def negative():
    while True:
        yield i

Previously we created applicative, that would be able to lift iterables/generators by doing the equivalent of:

def apply(wf,wx)
    return [f(x) 
        for f in wf 
        for x in wx]

This would of course only work for generators that end. But we could combne generators in a way that would be equal to:

In [30]:
def apply(wf,wx):
    return (f(x) 
        for f,x in zip(wf, wx))

So, how would we define the pure function? One way might be:

In [31]:
def pure(v):
    yield v

There is a problem with this definition, though. We want apply(pure(id),t) == t, for any t. And if we try i.e. t = [1, 2, 3]

In [35]:
list(apply(pure(lambda x:x),[1,2,3]))

This means that the correct definition for pure will need to be an infinite stream, repeating the value.

In [44]:
class ApplyGenerator(Applicative):
    def pure(self, val):
        while True:
            yield val

    def apply(self, wf, wx):
        return (f(x) 
            for f,x in zip(wf, wx))

Now we can try this with our window function.

In [72]:
list(ApplyGenerator().lift(window)([1,2], [5,6], [
[[2, 3, 4, 5], [3, 4, 5, 6]]

And it seems that it is working as expected :-)

Composing applicatives

Now, after we have defined several somewhat useful applicatives, we can nest them. I am stealing the implementation from the original paper Applicative programming with effects from the part Composing applicative functors

In [45]:
def containing(outer, inner):
    class Nested(Applicative):
        def pure(self, val):
            return outer.pure(inner.pure(val))
        def apply(self, lfn, lval):
            return outer.lift(inner.apply)(lfn,lval)
    return Nested()

Now we can make a i.e. a stream of possible errors.

In [46]:
ListOfValid = containing(ApplyGenerator(), ApplyError() )

You can see, that this still works with the vaules it have worked previously.

In [47]:
list(ListOfValid.lift(window)([1,2], [5,6], [
[[2, 3, 4, 5], [3, 4, 5, 6]]

But we can try to pass in some error values alongside of the iterables.

In [75]:
list(ListOfValid.lift(window)([1,Exception("Begin was negative")], [5,Exception("End was not positive")], [
[[2, 3, 4, 5], Exception('Begin was negative', 'End was not positive')]

You can see that the first values of the iterables were correctly combined into result, and second values that had some errors have the combined error result.

In [77]:
list(ListOfValid.lift(window)([Exception("Begin was negative"),Exception("Begin was negative")], [5, 6], [
[Exception('Begin was negative'), Exception('Begin was negative')]

Here we have error in both the first and the second and it results in both errors being shown in the resulting stream.