Partial application


Martin McBride, 2020-09-03
Tags partial application closure inner function function composition higher order function map
Categories functional programming

This article is part of a series on functional programming.

Functional programming, as we have seen, is a paradigm in which functions form the fundamental building blocks.

It should be no surprise that functional programming includes several techniques for deriving new functions from existing functions. This is analogous to object oriented programming, where classes are the building block, and we have various ways to derive new classes from existing ones (inheritance and composition, for example). In both cases, the aim is the same - to reuse existing code (following the DRY principle, don't repeat yourself).

Partial application is one such technique.

What is partial application?

Here is a function that returns the value of x, clamped to the range a to b. That is, it returns the value of x unless x is less than a (in which case a is returned), or x is greater than b (in which case b is returned):

def clamp(a, b, x):
    return min(b, max(a, x))

The partial method (in the standard functools module) can be used to create a new function, that behaves like clamp, but with some of its arguments already set to fixed values. For example:

from functools import partial

clamp_01 = partial(clamp, 0, 1)
print(clamp_01(2))  # Prints 1

Here, clamp_01 is a function that does the same job as clamp, but with parameter a set to 0 andb set to 1. This means that clamp_01 only accepts a single parameter, x, that gets clamped between 0 and 1.

We call this technique partial application, because you can think of it behaving as if the clamp function had been applied to a and b, but not yet applied to the final parameter x. This is sometimes described as binding the function to the values of a and b.

The important thing to notice about partial is that it is a higher order function - a function that operates on other functions. It accepts a function as its first argument, and returns a brand new function as a result. partial itself doesn't do any clamping, it just creates a new clamping function.

Example of using partial

The map function is often used in functional programming, to apply a function to a sequence of values. For example:

s = map(neg, [1, -2, 0 -3, 2])

This code applies the built-in neg function to the list of values, creating a sequence of numbers as a result. The neg function simply negates the value (1 becomes -1, -2 becomes 2, etc). So it will create an output sequence (-1, 2, 0, 3, -2). If you try this code remember that map creates a lazy sequence, so you will need to convert it to a list if you want to print it:

print(list(s))

Now the important thing about map is that the function you pass into it must accept exactly one parameter. For example neg accepts one parameter, neg(x) returns the negative of x.

If we wanted to use map to apply 0 to 1 clamping to a sequence of numbers, we couldn't use clamp(0, 1, x) because it takes 3 parameters (even though 2 of them are constant). That is where partial comes in. We can do this:

map(partial(clamp, 0, 1), [1, -2, 0 -3, 2])

Remember that partial returns a function - in this case a function that behaves like clamp except that a and b are fixed, so the partial function only takes a single parameter x. That is exactly what we need!

Why use partial?

Now you may be thinking that there are other ways of doing this. That is certainly true. For example, you could create a special clamp function like this:

def my_clamp(x):
    return clamp(0, 1, x)

map(my_clamp, [1, -2, 0 -3, 2])

Or, if you didn't want to go to the trouble of creating a named function that you only needed to use once, you could use a lambda:

map(lambda x: clamp(0, 1, x), [1, -2, 0 -3, 2])

There is nothing terribly wrong with either of these solutions - they work, and they aren't overly complicated, inefficient or messy.

But they are procedural. The definition of the my_clamp function doesn't just request a version of clamp that has preset a and b values. It includes code that specifies how to achieve that. The problem is that a line of procedural code could be doing anything. You have to read and understand the code to be sure it is really doing what it claims to be doing. Sure, it isn't that difficult with a one-line function, but still, every line of procedural code is an accident waiting to happen.

On the other hand, the partial call is declarative. You say exactly what you want, but not how to do it:

partial(clamp, 0, 1)

Here, you are very clearly stating that you want a version of clamp that has its first two parameters set to 0 and 1. The code couldn't possibly mean anything else, and assuming you trust the partial function (which is part of core Python so you probably can) there isn't really anything that could go wrong.

By analogy, if you were writing code to search for a values a Python list, you probably wouldn't write your own code to do it. You could, and it wouldn't be that difficult. But you wouldn't because it would be pointless, and random, and anyone maintaining your code would first wonder why you would do such a thing, and then probably check your code very carefully to check that there isn't some clever reason why you aren't just using the built in function.

The same is true of partial. If you are using a functional programming paradigm, and you need to do a standard thing to a function, such as obtaining a partial function, it is best to just use the standard functions to do it.

Other uses

Partial application can be applied multiple times, for example:

non_neg = partial(clamp, 0)
...
byte_value = partial(non_neg, 255)

In this case, the first call creates a function non_neg(b, x), which is a variant of clamp that clamps the minimum value to 0 but still allows you to choose an upper limit, b. That means that code using this function can definitely never produce negative output.

At some point later in your code, you can call partial again to create byte_value(x), a function that further limits the values to a maximum value of 255 (as well as the previous minimum of 0).

You can also apply optional parameters using partial:

csv = partial(print, sep=",")
csv(1, 2, 3) # 1,2,3
csv(1, 2, 3, sep = "|") 1|2|3

This example creates a new function csv, that behaves exactly like print except the separator is a comma by default. Notice that the csv function still allows you to override the separator if you wish.

Summary

Functional programming is a useful aspect of the Python language that you can mix and match with procedural code. If you decide to use FP, it is worth knowing some of the standard utility functions that Python provides.

See also

If you found this article useful you might be interested in my ebook Functional Programming in Python. It covers partial application in more depth.


Tag cloud

2d arrays abstract data type alignment and array arrays bezier curve built-in function close closure colour comparison operator comprehension context conversion data types design pattern device space dictionary duck typing efficiency encryption enumerate filter font font style for loop function function composition function plot functools generator gif gradient higher order function html image processing imagesurface immutable object index inner function input installing iter iterator itertools lambda function len linspace list list comprehension logical operator lru_cache mandelbrot map monad mutability named parameter numeric python numpy object open operator optional parameter or partial application path positional parameter print pure function radial gradient range recursion reduce rotation scaling sequence slice slicing sound spirograph str stream string subpath symmetric encryption template text text metrics transform translation transparency tuple unpacking user space vectorisation webserver website while loop zip

Copyright (c) Axlesoft Ltd 2020