Python

Explicit Currying in Python

Last week, I talked about currying a little bit, and I’ve been thinking about how one could do in languages that don’t have it built in. It can be done in Java, but the type of even a two-parameter function would be something like Function<Integer, Function<Integer, Integer>>, which is bad enough. Imagine 3- or 4-parameter functions.

There’s also the fact that calling curried functions doesn’t look good. Calling the two-parameter function with both parameters would look something like func.apply(1).apply(2). This isn’t what we want. At worst, we want to be stuck with func(1)(2), but that’s not possible in Java. At best, we want to be able to choose func(1)(2) (calling it with the first argument at one point in time and using the second one later) OR func(1, 2), depending on our current need. It’s possible to do so in python, so I’m using that.

How it Can be Done

All of this is possible in python thanks to default arguments and nested functions. Let’s check out the simplest case with two parameters again:

def func(a, b=None):
    def inner(b):
        return a + b
 
    if b is None:
        return inner
    else:
        return inner(b)

In this example, you can see that the outer function has the potential to accept arguments for both of the parameters, making the last one default to None. Inside, the curried-to function is defined to take only the second argument. After that, it checks to see if something was given for b, and if there has been, it calls the inner function with b (a is provided through closures). If something wasn’t given for b, it simply returns the inner function so that it can be called and provided with b later.

Going Bigger

From the previous example, you can probably derive how additional parameters can be added, but I’ll provide you with an additional example with three parameters:

def func(a, b=None, c=None):
    def inner1(b, c=None):
        def inner2(c):
            return a + b + c
 
        if c is None:
            return inner2
        else:
            return inner2(c)
 
    if b is None:
        return inner1
    else:
        return inner1(b, c)

There are a few things to note here. First off, you might be surprised that you don’t have to make a distinction between b being given but not c and both b and c being given. My first instinct was that you would have to check that, but you don’t. Why not? Because inner1 checks c and decides whether inner2 needs to be returned or called.

Restrictions

There are some severe limitations to setting up functions to be curried like this. The first is that you functionally only get a fixed-length set of position-based parameters. No *args (except as the very last parameter and no **kwargs. You also don’t even get to truly use default arguments, since their use for currying would clash with their intended use. There may be a few cases that work as exceptions, but they would require extra thought and workarounds to accomplish.

You’ll also want to very carefully document the use of these curried functions so that people will understand how they work and how to use them.

Alternative Defaults

There may come a time when you want to be able to accept None as a valid argument. In such a case, you could create a dummy object to represent when a value isn’t provided. For example:

class _NotGiven():
    pass
 
NotGiven = _NotGiven()

You have your hidden class (hidden so that people don’t foolishly make instances of it) and a “constant” instance of the class that can be used (more explanation as to why in a little bit).

With this in place, you could rewrite the first example like this:

def func(a, b=NotGiven):
    def inner(b):
        return a + b
 
    if b is NotGiven:
        return inner
    else:
        return inner(b)

You see, using the “constant” of NotGiven instead of always making an instance of _NotGiven allows us to reduce memory used by object creation as well as allowing the checks to read more fluently. i.e. “if b is not given” as opposed to “if is instance b not given” that you would get with if isinstance(b, _NotGiven):.

Pros and Cons

Cons

Generally, you’ll want to actually avoid using this technique. Largely, it is not a worthwhile exercise. Being that it’s an explicit implementation of currying, it is more complicated to read and write than it should generally be. There are also not a lot of cases of functions being partially called, even with the relatively simple partial function provided with python. Even then, though, using partial isn’t a bad idea.

Pros

BUT, there may be times when you know that a function will be used partially, and only sometimes. If you know a function is going to be used partially ALL the time, you simplify its definition, taking out the later parameters on the outermost function and skipping the check, returning the inner function automatically. A big upshot to these curried functions over partial is the ability to read it in usage. In the rare instance that it is usable, writing explicitly curriable functions might be a life saver. Keep them as a tool in your back pocket: rarely used, but ready and waiting for the chance to be used every once in a while.

Reference: Explicit Currying in Python from our WCG partner Jacob Zimmerman at the Programming Ideas With Jake blog.

Jacob Zimmerman

Jacob is a certified Java programmer (level 1) and Python enthusiast. He loves to solve large problems with programming and considers himself pretty good at design.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button