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. |