Currying explained for Python programmers
23 Jun 2015One thing that strikes Haskell beginners with its weirdness is the notion of a curried function. Since all functions are curried by default in Haskell, it’s safe to say that it’s essential to understand what currying is to be able to delve deeper into Haskell.
Although the idea of currying is not particularly hard to grasp, it sounds a bit weird since it’s kind of a crazy way of approaching functions—especially due to the tuple-based ontology familiar from calculus classes.
Before even trying to define what currying is at an abstract level, let’s go ahead and define a “function”1
f
in Python to fiddle with its behavior.
>>> def f(x, y, z): return x**3 + y**2 + z
...
>>> f(2, 2, 2)
14
f
is simply a function that returns f(2)
. Of course as expected, Python passively-aggressively tells
us that we made the mistake of not providing f
with enough arguments.
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: f() takes exactly 3 arguments (1 given)
This is the expected behavior with the common conception of a
function; a function must be fed the number of arguments it requires. In Haskell though, all functions show curried behavior: they are
defined as the sequence of multiple single-argument functions. So if I
were to define the same function f
in Haskell, the application
of f
to a single argument would return another function
expecting to operate on the remaining two arguments. If I were to
apply that function to another single argument, that would return yet
another function expecting to operate on the third one of the three arguments f
accepts in total. You get the idea.
To make things a bit more concrete, let’s make ourselves a curried function in Python.
Let’s play with our curried f
; if we call it with a single
argument, we get a new function that we will store to g
.
>>> curried_f(2, 3, 4)
21
>>> g = curried_f(2)
>>> g(3, 4)
21
Or, we can do this
>>> g = curried_f(2)
>>> h = g(3)
>>> h(4)
21
But why would anyone want this? The answer may not be apparent in
Pythonian thinking. But consider map
, for example. We can
map
over a list if we have a function expecting a single
argument. If we were to use a function that takes multiple arguments, we
would have to use lambda
or define a new function. But with this
curried function we made we could compute
>>> map(curried_f(4, 3), [1, 2, 3, 4])
[74, 75, 76, 77]
Or we could plug it into reduce
like:
>>> reduce(curried_f(2), [1, 2, 3, 4])
17436
Of course, these high-level operations such as map
and
reduce
are not used that frequently in Python, but they go so
well with Haskell’s take on programming that being able to get things
done with these higher level functions is kind of like the Haskell
programmer’s idiomatic competence. If these stuff sound interesting to you, definitely take a look at what Miran Lipovača has to say about them.
-
I put quotation marks there because the term function in Python does not extend beyond being a useful analogy. Python functions are not functions in the same sense that Haskell functions are functions; the latter are actual mappings between sets. ↩