Understanding function composition

The foundation of programming is to take smaller abstractions and make bigger abstractions out of it. In the previous articles in this series, we learned about using functions to create abstractions. Now let us look at how to join them up together using functional composition.

In the previous articles in this series, we learnt about using functions to create abstractions. Now let us look at how to join them up together using functional composition.

We start our example with these three functions

def twice(n):
    return 2 * n

def square(n):
    return n * n

def half(n):
    return n / 2

We now want to create a function that can calculate (n *  n)/2. We already have a function square and a function half with us. We need to create a function that is the combination of these two functions.

Let us create a function that can combine any two functions. This operation is called function composition.

Function Composition

At this point, you might stop me and say why not just write a new function like this

def square_and_half(n):
    return half(square(n))

The problem is that this function is hardcoded to do square followed by half. We will have to write a separate function if we want to combine twice and square. And what if we want the user to select the functions and then combine them dynamically at runtime?

For these reasons, let us create a separate function to do this composition. You probably can guess the approach – higher order functions. Here is the compose function

def compose(first_fn, second_fn):
    def composed_fn(*args, **kwargs):
        return second_fn(first_fn(*args, **kwargs))
    return composed_fn

The compose function takes two other functions as input, and returns a function as output. The output function will execute the two input functions one after the other.

fn = compose(square, half)
fn(4) # 8

fn = compose(twice, square)
fn(4) # 64

As we can see above, we can combine any two functions and get a composed function as output.

You can extend this to a chain of more functions. If we want to do twice -> square -> half then we do this

fn = compose(compose(twice, square), half)
fn(4) # 32

Of course, it would be much nicer to extend the implementation of  compose to accept any number of input functions, so that one could just do compose(twice, square, half). That is a fun exercise to try as a homework 😊

Composing functions with many inputs

All the functions twice, square and half are unary – they all take only a single input. So the output of twice can go as in input to square, and the output of square can go as an input to half.

What if the functions are not unary? The add function below is a binary function (binary – takes 2 inputs)

def add(a, b):
    return a + b

We now want to create a function to evaluate the expression (5 + 2*n)/2. We can easily calculate 2*n using the twice function, and the output of that needs to go as an input to add. The problem is, add takes two inputs, not one. The second input to add is 5. How do we compose it?

Take a moment to think about this before proceeding further.

The answer is to use partial application that we discussed in the article on higher order functions. Using partial, we will partially apply the 5 parameter to the add function.

from functools import partial

add5 = partial(add, 5)

Now we have a function that takes a single parameter, and we can compose it easily

fn = compose(compose(twice, add5), half)
fn(4) # 6.5

And that's it. Using these techniques, you can compose any number of functions taking any number of parameters.

Abstraction and composition

Composition is really a simple topic, but it is very important as this is the glue that allows us to combine functions together to build bigger and bigger abstractions.

Think about it like lego bricks, the holes that allow the bricks to fit together look simple, but are so important. Once you have that, you can take basic bricks and fit them together to make walls, take walls and fit them to make buildings, take buildings and make streets, take streets and make cities...

Lego city. Image from Columbus Museum of Art

When you are making a wall, you need to think about where every brick goes. When making the building you are thinking "I need a wall here, and a wall here and a roof here, put a door here and some pillars besides the door". So you are not thinking about individual bricks anymore, you are thinking in terms of walls, roof, door and pillars. And when laying out the city you might be thinking, "this building goes here, there is a garden around it, and roads around it. The second building goes on the other side of the road...". so now the thinking is at an even higher level language of buildings, gardens, and roads.

That is essentially abstraction in action, and what is true for lego is the same for programming. Our goal is to create small abstractions, combine them into bigger abstractions, then combine those into still bigger abstractions and so on until we get our final program. Each level of abstraction provides us a certain "language" of thinking which we can use to solve bigger problems. And composition is the glue that sticks all this together.

In the next two articles, we will take all that we have learnt so far and put it together to solve some problems.

Comments

Sign in or become a Playful Python member to join the conversation.
Just enter your email below to get a log in link.