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.
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
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
def compose(first_fn, second_fn): def composed_fn(*args, **kwargs): return second_fn(first_fn(*args, **kwargs)) return composed_fn
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
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
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
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
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...
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.