7.5. Other Examples of Higher Order Functions¶
In this section, we will implement some other common higher-order functions
Many of these functions are implemented in the Python libraries toolz
and
cytoolz
under the sub module titled functoolz
. These two modules have
exactly the same functionality, but the cytoolz
functions are implemented
within cython
, which means that you get better performance. Unfortunately,
you need a C
compiler to install cytoolz
, but toolz
also has decent
performance approximately equal in performance to similar tools from the
standard library.
Note
You can install these packages using pip
from the command line, as shown
below,
pip install toolz cytoolz
or you can run this command inside a jupyter console or notebook.
!pip install toolz cytoolz
7.5.1. Composition¶
Consider the following example, which involves cleaning up a string by removing punctuation, whitespace and upper-case characters.
In [1]: from string import punctuation, whitespace
In [2]: s = '''Success is not final,
...: failure is not fatal:
...: it is the courage to continue that counts.'''
...:
In [3]: remove_punc = lambda s: "".join([ch for ch in s if ch not in punctuation])
In [4]: make_lower_case = lambda s: s.lower()
In [5]: fix_whitespace = lambda s: "".join([" " if ch in whitespace else ch for ch in s])
In [6]: s = remove_punc(s)
In [7]: s = make_lower_case(s)
In [8]: s = fix_whitespace(s)
In [9]: s
Out[9]: 'success is not final failure is not fatal it is the courage to continue that counts'
In this imperative example, we are mutating the value stored as s
by
applying one function after the other. We could have composed these three calls
into one call, as follows.
In [10]: s = remove_punc(make_lower_case(fix_whitespace(s)))
In [11]: s
Out[11]: 'success is not final failure is not fatal it is the courage to continue that counts'
What if we want to reuse this code? Of course, we could use a function to make this expression reusable.
In [12]: clean_up = lambda s: remove_punc(make_lower_case(fix_whitespace(s)))
In [13]: s = clean_up(s)
In [14]: s
Out[14]: 'success is not final failure is not fatal it is the courage to continue that counts'
Function composition is so common that it will be useful to abstract the process of composing and applying a function.
Before we construct this function, it is useful to look at the imperative
solution to the problem. We can use a for loop to cycle through the functions
and an accumulator to keep track of the latest value of the output. Also note
that we need to apply the right-most function first, so we will used
reversed
to iterate through the list back to front. In addition, we add a
variable number of args to remove the need for using a list as an argument.
In [15]: def compose_imperative(*funcs): #*
....: def new_func(item):
....: output = item
....: for func in reversed(funcs):
....: output = func(output)
....: return output
....: return new_func
....:
In [16]: clean_up = compose_imperative(remove_punc, make_lower_case, fix_whitespace)
In [17]: clean_up(s)
Out[17]: 'success is not final failure is not fatal it is the courage to continue that counts'
The fact that we are using the accumulator pattern indicates that this operation is a reduction. Creating this function adheres to the DRY principle, as we won’t need to explicitly compose functions over and over in our code. In fact, we don’t even need to give this new function a name, but can call it anonymously as follows.
In [18]: compose_imperative(remove_punc,
....: make_lower_case,
....: fix_whitespace)(s)
....:
Out[18]: 'success is not final failure is not fatal it is the courage to continue that counts'
Now that we have recognized that this process is a reduction, we refactor the code accordingly. In this case, it is again important to work through the reversed list to preserve the order of operation of functional composition.
In [19]: from functools import reduce
In [20]: def my_compose(*funcs): #*
....: def new_func(item):
....: return reduce(lambda acc, next_func: next_func(acc), reversed(funcs), item)
....: return new_func
....:
In [21]: my_compose(remove_punc,
....: make_lower_case,
....: fix_whitespace)(s)
....:
Out[21]: 'success is not final failure is not fatal it is the courage to continue that counts'
There is no need to implement this function, as the toolz
library includes
an implementation called compose
.
In [22]: from toolz import compose
In [23]: compose(remove_punc,
....: make_lower_case,
....: fix_whitespace)(s)
....:
Out[23]: 'success is not final failure is not fatal it is the courage to continue that counts'
7.5.2. Other types of composition¶
One must always remember that compose
passes the input through the functions
from right-to-left. Another composition function included in toolz
, pipe
,
can be used to push an argument through any number of unary functions from
left-to-right.
In [24]: from toolz import pipe
In [25]: pipe(s , fix_whitespace, make_lower_case, remove_punc)
Out[25]: 'success is not final failure is not fatal it is the courage to continue that counts'
The pipe
function is designed for a sequence of unary (one argument)
functions. What if we want to perform a similar, left-to-right sequence of
calls, but with functions with arity greater than 1? Use tread_first
! The
first argument of thread_first
is the argument val
that will be passed
through a sequence of functions. The remaining argument are the functions that
will be allied from left-to-right. thread_first
allows a function argument
to be replaced with a tuple in the form of (func, arg1, arg2, ..., argn)
,
and the subsequent function call will be of the form func(val, arg1, arg2,
..., argn)
. Note that the first in tread_first
indicated that the
argument val
will be the first argument in each call.
In [26]: from operator import add, pow, abs, sub
In [27]: from toolz import thread_first
In [28]: thread_first(5, (add, 2), (pow, 2), (sub, 6))
Out[28]: 43
# The above is equivalent to
In [29]: sub(pow(add(5, 2), 2), 6)