Chapter 04: Currying

Can't Live If Livin' Is without You

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+y
increment = add(1)
add_ten = add(10)
increment(2) #3
add_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.

More Than a Pun / Special Sauce

Currying is useful for many things. We can make new functions just by giving our base functions some arguments as seen in has_letter_r, remove_strings_without_rs, and censored.

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 map:

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 partial
all_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 map:

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

In Summary

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