Function Composition and Currying In Python

Composition and currying are features of functional programming languages that can be very convenient in some situations. Haskell supports composition and currying directly, so as a spoiled Haskeller, I don't want to do without those luxuries when I need to use Python. Fortunately you can easily setup your python program to do this for you.

First let me explain what these things are.

Function Composition

Composition is when you apply multiple functions one after the other to the same input. So if you want to first apply h to x, and then apply g to the result, and then apply f to the result, it is the same as composing f, g, and h first, and then applying the composed function to x. This comes directly from math, in math notation composition is represented by a circle:

$$f(g(h(x))) = (f \circ g \circ h) (x)$$

In Haskell you can just just compose functions using the dot operator, inspired by the composition circle in math. These expressions are all doing the same thing, defining the function fgh as a composition of f, g, and h:



-- composition defined explicitly using the argument x
-- as it is done in most programming languages
fgh x = f (g (h x))

-- lambda notation for the same thing. This still defines 
-- composition explicitly using the argument x
fgh = \x -> f (g (h x))


-- composition defined without having to specify an argument
-- "point free" as it is done in math
fgh = f . g . h

In python, if we want to compose the functions f, g, and h, we have to define the function fgh explicitly, as in the first Haskell expressions. The language doesn't provide directly a way to do point free composition. The lambda notation in Python is like the lambda notation in Haskell, it defines an anonymous function, but it still needs us to use the argument x in the definition of the function.

# composition defined explicitly using the argument x
# as it is done in most programming languages
def fgh(x):
    return f(g(h(x)))

# lambda notation for the same thing. This still defines 
# composition explicitly using the argument x
fgh = lambda x: f(g(h(x)))

But Python is a functional programming language, so we should be able to define what we want. Turns out defining a function to compose other functions is pretty easy. We can't do a nice dot operator syntax like in Haskell, but at least we can pass a list of functions to the compose function.


# compose a list of functions [f, g, h] as f(g(h(x)))
def compose(functions):
    def composed_function(x):
        for f in reversed(functions):
            x = f(x)
        return x
    return composed_function

# composition defined without having to specify an argument
# "point free" as it is done in math
fgh = compose([f, g, h])

Function Currying

Currying is partial application of a function with many arguments. Let's say you have a function that takes two arguments. With currying you can call the original two-argument function with only one argument, and it will return a new one-argument function that only requires the missing argument. This comes from the lambda calculus, where the building blocks for functions are lambdas, anonymous functions that can only have one argument. Under this restriction, a function of two arguments is defined as a function of one argument that returns another function of one argument, and that second function returns what we wanted on the first place. For example in lambda calculus notation a function of x and y that adds x and y is defined as a function of x than returns a function of y that returns the sum of x and y

$$\text{add}\ x \ y = \lambda x. \lambda y . (x + y)$$

In Haskell that is also part of the language since it is based in lambda calculus, the standard definition is just syntactic sugar for lambda calculus notation, so currying comes for free with the language.

-- function of two arguments in Haskell
-- this under the hood is a lambda calculus expression
add :: Int -> Int -> Int
add x y = x + y

-- Same as above written in lambda notation
add :: Int -> Int -> Int
add = \x -> \y -> x + y

-- curried function of one argument created by partial application 
-- binding x to 10 returns the function \y -> 10 + y
add10 :: Int -> Int
add10 = add 10

-- supplying a second argument to the curried function
-- binds y to 5 returns 10 + 5
add10 :: Int
add10 5

Python is not built on top of lambda calculus, so it doesn't support currying out of the box, but we can build a function for it too. The nice thing about this function is that it can be used as a decorator, so we can make any function curried very easily.

# make a function curried, so that f(a, b, c) can be called
# as f(a)(b)(c) or f(a,b)(c) or f(a)(b, c) 
def curry(f):
    def curried(*args):
        if len(args) == f.__code__.co_argcount:
            return f(*args)
        return lambda x: curried(*args, x)
    return curried

# option 1, take an existing function and get a curried version
def f_original_uncurried(a, b, c):
    return (a - b) / c

f_curried_later = curry(f_original_uncurried)

# option 2, use a decorator "@curry" above the definition
# the function is curried without having to call curry directly

@curry
def f_original_curried(a, b, c):
    return (a - b) / c

Function Composition And Currying Together

Composition and currying work very well together to create new functions. For example, let's say we want to first square a number, then multiply it by two, then add 100 to the result, and we have already defined functions to add, multiply, and square, composition and currying make it very easy to define the new function.

Here is how we do it in Haskell:

add :: Int -> Int -> Int
add x y = x + y

multiply :: Int -> Int -> Int
multiply x y = x * y

square :: Int -> Int
square x = x * x

newfunction :: Int -> Int
newfunction = add 100 . multiply 2 .  square

Here is how we do it in Python after defining the functions for currying and composition:

@curry
def add(x, y):
    return x + y

@curry
def multiply(x, y):
    return x * y

# no need to curry a single argument function
def square(x):
    return x*x

newfunction = compose([add(100), multiply(2), square])

This is a toy example of course, but there are real life situations where this ability could be very useful. For example, let's say you have an extensive library of transformations that you could apply to a dataset to preprocess it looking for a signal, or feature engineering. If you don't know yet which transformations in which order would be useful and you are just writing exploratory code to see what they do to the data, it would be very nice to be able to define functions via composition and currying rather than having to hand code every single combination you want to try.

Currying Named Parameters

One problem with this implementation is that our arguments have to be in order, if we had a function divide(num, denom), and we want to make a curried function that divides by two, we want to fix the denominator and leave the numerator as the free variable. In Haskell we have the flip function for that, but in Python we don't have that option. We could use a lambda instead of curry for that situation, or we could build a version of curry that requires keyword names for its arguments. Let's look at both scenarios.

def divide(num, denom):
    return num / denom

# option 1, use a lambda if we want the first variable to be free
fun1 = compose([lambda x: divide(x, 2), add(100), multiply(2), square])

# option2, define a curry that uses keyword arguments for all except the last 
# free variable. This allows us to curry out of order
def kwcurry(f):
    def curried(**kwargs):
        required_args = f.__code__.co_varnames
        provided_args = kwargs.keys()
        missing_args = [arg for arg in required_args if arg not in provided_args]
        if len(missing_args) == 1: #curried with one arg, ready to use
            return lambda x: f(**kwargs, **{missing_args[0]: x})
        elif len(missing_args) == 0: # no missing args, just call the function
            return f(**kwargs)
        else: #needs more currying
            return lambda **more_kwargs: curried(**{**kwargs, **more_kwargs})
    return curried

fun2 = compose([kwcurry(divide)(denom=2), add(100), multiply(2), square])

It's good to know we can write something like the kwcurry function in Python, but just because we can it doesn't mean we should. Unlike the curry function, it's not a good idea to use the kwcurry function as a decorator because it changes how the decorated function is called. This is a narrow use case that the lambda notation can handle just fine, so writing kwcurry is definitely overkill. On the other hand, it's fun to learn how Python works under the hood, and writing functions like kwcurry is a good way to take a look and get your hands dirty, just don't check it into your codebase!

0
Subscribe to my newsletter

Read articles from Francisco Gutierrez directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Francisco Gutierrez
Francisco Gutierrez