My Dad once explained how there are certain things one can live without until one acquires them. A microwave is one such thing. Smart phones, another. The older folks among us will remember a fulfilling life sans internet. For me, currying is on this list.
The concept is simple: You can call a function with fewer arguments than it expects. It returns a function that takes the remaining arguments.
You can choose to call it all at once or simply feed in each argument piecemeal.
add = lambda x: lambda y: x+yincrement = add(1)add_ten = add(10)increment(2) #3add_ten(2) #12
Here we've made a function
add that takes one argument and returns a function. By calling it, the returned function remembers the first argument from then on via the closure. Calling it with both arguments all at once is a bit of a pain, however, so we can use a special helper function called
curry to make defining and calling functions like this easier.
Let's set up a few curried functions for our enjoyment. From now on, we'll summon our
curry function defined in the Appendix A - Essential Function Support.
match = curry(lambda what, s: re.findall(what,s))replace = curry(lambda what, replacement, s: re.sub(what, replacement, s))filter = curry()map = curry(lambda f, xs: [f(x) for x in xs])
The pattern I've followed is a simple, but important one. I've strategically positioned the data we're operating on (str, list) as the last argument. It will become clear as to why upon use.
match('r', 'hello world'); # ['r']has_letter_r = match('r') # lambda s: re.findall('r',s)has_letter_r('hello world') # ['r']has_letter_r('just j and s and t etc') # filter(has_letter_r, ['rock and roll', 'smooth jazz']) # ['rock and roll']remove_strings_without_rs = filter(has_letter_rs) # lambda xs: [x for x in xs if re.findall("r",x)]remove_strings_without_rs(['rock and roll', 'smooth jazz', 'drum circle']) # ['rock and roll', 'drum circle']no_vowels = replace(r'[aeiou]') # lambda r,x: re.sub(r'[aeiou]', r, x)censored = no_vowels('*') # lambda x: re.sub(r'[aeiou]', '*', x)censored('Chocolate Rain'); # 'Ch*c*l*t* R**n'
What's demonstrated here is the ability to "pre-load" a function with an argument or two in order to receive a new function that remembers those arguments.
I encourage you to clone the Mostly Adequate repository (
git clone https://github.com/tartavull/mostly-adequate-guide.git), copy the code above and have a go at it in the REPL. The curry function, as well as actually anything is defined in the appendixes.
Currying is useful for many things. We can make new functions just by giving our base functions some arguments as seen in
We also have the ability to transform any function that works on single elements into a function that works on arrays simply by wrapping it with
get_children = lambda x: x['children']all_the_children = map(get_children)
We can also use
functools.partial, which is more verbose than just currying:
from functools import partialall_the_children = partial(map, get_children)
Giving a function fewer arguments than it expects is typically called partial application. Partially applying a function can remove a lot of boiler plate code. Consider what the above
all_the_children function would be with the uncurried
all_the_children = lambda elements: map(elements, get_children)
We typically don't define functions that work on arrays, because we can just call
map(get_children) inline. Same with
filter, and other higher order functions (a higher order function is a function that takes or returns a function).
When we spoke about pure functions, we said they take 1 input to 1 output. Currying does exactly this: each single argument returns a new function expecting the remaining arguments. That, old sport, is 1 input to 1 output.
No matter if the output is another function - it qualifies as pure. We do allow more than one argument at a time, but this is seen as merely removing the extra
()'s for convenience.
Currying is handy and I very much enjoy working with curried functions on a daily basis. It is a tool for the belt that makes functional programming less verbose and tedious.
We can make new, useful functions on the fly simply by passing in a few arguments and as a bonus, we've retained the mathematical function definition despite multiple arguments.
Let's acquire another essential tool called