1.6 Higher-Order Functions
1.6 Higher-Order Functions
6 Higher-Order Functions
>>> 3 * 3
1.2.1 Expressions
9
1.2.2 Call Expressions >>> 5 * 5
1.2.3 Importing Library Functions 25
1.2.4 Names and the
Environment and never mentioning square explicitly. This practice would suffice for simple computations such as
1.2.5 Evaluating Nested square , but would become arduous for more complex examples such as abs or fib . In general, lacking
Expressions function definition would put us at the disadvantage of forcing us to work always at the level of the
1.2.6 The Non-Pure Print particular operations that happen to be primitives in the language (multiplication, in this case) rather
Function than in terms of higher-level operations. Our programs would be able to compute squares, but our
language would lack the ability to express the concept of squaring.
1.3 Defining New Functions One of the things we should demand from a powerful programming language is the ability to build
abstractions by assigning names to common patterns and then to work in terms of the names directly.
1.3.1 Environments
Functions provide this ability. As we will see in the following examples, there are common
1.3.2 Calling User-Defined
programming patterns that recur in code, but are used with a number of different functions. These
Functions
patterns can also be abstracted, by giving them names.
1.3.3 Example: Calling a User-
Defined Function To express certain general patterns as named concepts, we will need to construct functions that can
1.3.4 Local Names accept other functions as arguments or return functions as values. Functions that manipulate
1.3.5 Choosing Names functions are called higher-order functions. This section shows how higher-order functions can serve
1.3.6 Functions as Abstractions as powerful abstraction mechanisms, vastly increasing the expressive power of our language.
1.3.7 Operators
www.composingprograms.com/pages/16-higher-order-functions.html 1/12
2023/9/2 10:31 1.6 Higher-Order Functions
1.7.3 Printing in Recursive These three functions clearly share a common underlying pattern. They are for the most part
Functions identical, differing only in name and the function of k used to compute the term to be added. We could
1.7.4 Tree Recursion generate each of the functions by filling in slots in the same template:
1.7.5 Example: Partitions
def <name>(n):
total, k = 0, 1
while k <= n:
total, k = total + <term>(k), k + 1
return total
The presence of such a common pattern is strong evidence that there is a useful abstraction waiting
to be brought to the surface. Each of these functions is a summation of terms. As program designers,
we would like our language to be powerful enough so that we can write a function that expresses the
concept of summation itself rather than only functions that compute particular sums. We can do so
readily in Python by taking the common template shown above and transforming the "slots" into
formal parameters:
In the example below, summation takes as its two arguments the upper bound n together with the
function term that computes the kth term. We can use summation just as we would any function, and it
expresses summations succinctly. Take the time to step through this example, and notice how binding
cube to the local names term ensures that the result 1*1*1 + 2*2*2 + 3*3*3 = 36 is computed correctly.
In this example, frames which are no longer needed are removed to save space.
Using an identity function that returns its argument, we can also sum natural numbers using exactly
the same summation function.
The summation function can also be called directly, without definining another function for a specific
sequence.
We can define pi_sum using our summation abstraction by defining a function pi_term to compute each
term. We pass the argument 1e6 , a shorthand for 1 * 10^6 = 1000000 , to generate a close
approximation to pi.
www.composingprograms.com/pages/16-higher-order-functions.html 2/12
2023/9/2 10:31 1.6 Higher-Order Functions
3.141592153589902
This improve function is a general expression of repetitive refinement. It doesn't specify what problem
is being solved: those details are left to the update and close functions passed in as arguments.
Among the well-known properties of the golden ratio are that it can be computed by repeatedly
summing the inverse of any positive number with 1, and that it is one less than its square. We can
express these properties as functions to be used with improve .
Above, we introduce a call to approx_eq that is meant to return True if its arguments are approximately
equal to each other. To implement, approx_eq , we can compare the absolute value of the difference
between two numbers to a small tolerance value.
Calling improve with the arguments golden_update and square_close_to_successor will compute a finite
approximation to the golden ratio.
By tracing through the steps of evaluation, we can see how this result is computed. First, a local
frame for improve is constructed with bindings for update , close , and guess . In the body of improve , the
name close is bound to square_close_to_successor , which is called on the initial value of guess . Trace
through the rest of the steps to see the computational process that evolves to compute the golden
ratio.
www.composingprograms.com/pages/16-higher-order-functions.html 3/12
2023/9/2 10:31 1.6 Higher-Order Functions
Edit code in Online Python Tutor
This example illustrates two related big ideas in computer science. First, naming and functions allow
us to abstract away a vast amount of complexity. While each function definition has been trivial, the
computational process set in motion by our evaluation procedure is quite intricate. Second, it is only
by virtue of the fact that we have an extremely general evaluation procedure for the Python language
that small components can be composed into complex processes. Understanding the procedure of
interpreting programs allows us to validate and inspect the process we have created.
As always, our new general method improve needs a test to check its correctness. The golden ratio
can provide such a test, because it also has an exact closed-form solution, which we can compare to
this iterative result.
For this test, no news is good news: improve_test returns None after its assert statement is executed
successfully.
The above examples demonstrate how the ability to pass functions as arguments significantly
enhances the expressive power of our programming language. Each general concept or equation
maps onto its own short function. One negative consequence of this approach is that the global frame
becomes cluttered with names of small functions, which must all be unique. Another problem is that
we are constrained by particular function signatures: the update argument to improve must take exactly
one argument. Nested function definitions address both of these problems, but require us to enrich
our environment model.
Let's consider a new problem: computing the square root of a number. In programming languages,
"square root" is often abbreviated as sqrt . Repeated application of the following update converges to
the square root of a :
This two-argument update function is incompatible with improve (it takes two arguments, not one), and
it provides only a single update, while we really care about taking square roots by repeated updates.
The solution to both of these issues is to place function definitions inside the body of other definitions.
Like local assignment, local def statements only affect the current local frame. These functions are
only in scope while sqrt is being evaluated. Consistent with our evaluation procedure, these local def
statements don't even get evaluated until sqrt is called.
Lexical scope. Locally defined functions also have access to the name bindings in the scope in
which they are defined. In this example, sqrt_update refers to the name a , which is a formal parameter
of its enclosing function sqrt . This discipline of sharing names among nested definitions is called
lexical scoping. Critically, the inner functions have access to the names in the environment where
they are defined (not where they are called).
We require two extensions to our environment model to enable lexical scoping.
1. Each user-defined function has a parent environment: the environment in which it was defined.
2. When a user-defined function is called, its local frame extends its parent environment.
Previous to sqrt , all functions were defined in the global environment, and so they all had the same
parent: the global environment. By contrast, when Python evaluates the first two clauses of sqrt , it
create functions that are associated with a local environment. In the call
www.composingprograms.com/pages/16-higher-order-functions.html 4/12
2023/9/2 10:31 1.6 Higher-Order Functions
the environment first adds a local frame for sqrt and evaluates the def statements for sqrt_update and
sqrt_close .
Function values each have a new annotation that we will include in environment diagrams from now
on, a parent. The parent of a function value is the first frame of the environment in which that function
was defined. Functions without parent annotations were defined in the global environment. When a
user-defined function is called, the frame created has the same parent as that function.
Subsequently, the name sqrt_update resolves to this newly defined function, which is passed as an
argument to improve . Within the body of improve , we must apply our update function (bound to
sqrt_update ) to the initial guess x of 1. This final application creates an environment for sqrt_update that
begins with a local frame containing only x , but with the parent frame sqrt still containing a binding for
a.
Global fun
1 def average(x, y):
2 return (x + y)/2 average
fun
3 improve
4 def improve(update, close, guess=1):
approx_eq fun
5 while not close(guess):
sqrt
6 guess = update(guess) fun
7 return guess
8 f1: sqrt [parent=Global] fun
9 def approx_eq(x, y, tolerance=1e-3): a 256
fun
10 return abs(x - y) < tolerance sqrt_update
11
sqrt_close
12 def sqrt(a):
13 def sqrt_update(x):
f2: improve [parent=Global]
14 return average(x, a/x)
15 def sqrt_close(x): update
16 return approx_eq(x * x, a) close
17 return improve(sqrt_update, sqrt_close) 1
guess
18
Edit code in Online Python Tutor
f5: sqrt_update [parent=f1]
x 1
< Back Step 15 of 86 Forward >
The most critical part of this evaluation procedure is the transfer of the parent for sqrt_update to the
frame created by calling sqrt_update . This frame is also annotated with [parent=f1] .
Extended Environments. An environment can consist of an arbitrarily long chain of frames, which
always concludes with the global frame. Previous to this sqrt example, environments had at most two
www.composingprograms.com/pages/16-higher-order-functions.html 5/12
2023/9/2 10:31 1.6 Higher-Order Functions
frames: a local frame and the global frame. By calling functions that were defined within other
functions, via nested def statements, we can create longer chains. The environment for this call to
sqrt_update consists of three frames: the local sqrt_update frame, the sqrt frame in which sqrt_update
was defined (labeled f1 ), and the global frame.
The return expression in the body of sqrt_update can resolve a value for a by following this chain of
frames. Looking up a name finds the first value bound to that name in the current environment.
Python checks first in the sqrt_update frame -- no a exists. Python checks next in the parent frame, f1 ,
and finds a binding for a to 256.
Hence, we realize two key advantages of lexical scoping in Python.
The names of a local function do not interfere with names external to the function in which it is
defined, because the local function name will be bound in the current local environment in which
it was defined, rather than the global environment.
A local function can access the environment of the enclosing function, because the body of the
local function is evaluated in an environment that extends the evaluation environment in which it
was defined.
The sqrt_update function carries with it some data: the value for a referenced in the environment in
which it was defined. Because they "enclose" information in this way, locally defined functions are
often called closures.
The environment diagram for this example shows how the names f and g are resolved correctly, even
in the presence of conflicting names.
17 result = square_successor(12)
f2: h [parent=f1]
Edit code in Online Python Tutor
x 12
Return
169
< Back End Forward > value
x 13
Return
169
value
www.composingprograms.com/pages/16-higher-order-functions.html 6/12
2023/9/2 10:31 1.6 Higher-Order Functions
The 1 in compose1 is meant to signify that the composed functions all take a single argument. This
naming convention is not enforced by the interpreter; the 1 is just part of the function name.
At this point, we begin to observe the benefits of our effort to define precisely the environment model
of computation. No modification to our environment model is required to explain our ability to return
functions in this way.
A newton_update expresses the computational process of following this tangent line to 0, for a function f
and its derivative df .
Finally, we can define the find_root function in terms of newton_update , our improve algorithm, and a
comparison to see if f (x) is near 0.
Computing Roots. Using Newton's method, we can compute roots of arbitrary degree n. The degree
n root of a is x such that x ⋅ x ⋅ x … x = a with x repeated n times. For example,
The square (second) root of 64 is 8, because 8 ⋅ 8 = 64.
The cube (third) root of 64 is 4, because 4 ⋅ 4 ⋅ 4 = 64.
The sixth root of 64 is 2, because 2 ⋅ 2 ⋅ 2 ⋅ 2 ⋅ 2 ⋅ 2 = 64.
We can compute roots using Newton's method with the following observations:
www.composingprograms.com/pages/16-higher-order-functions.html 7/12
2023/9/2 10:31 1.6 Higher-Order Functions
If we can find a zero of this last equation, then we can compute degree n roots. By plotting the curves
for n equal to 2, 3, and 6 and a equal to 64, we can visualize this relationship.
The approximation error in all of these computations can be reduced by changing the tolerance in
approx_eq to a smaller number.
As you experiment with Newton's method, be aware that it will not always converge. The initial guess
of improve must be sufficiently close to the zero, and various conditions about the function must be
met. Despite this shortcoming, Newton's method is a powerful general computational method for
solving differentiable equations. Very fast algorithms for logarithms and large integer division employ
variants of the technique in modern computers.
1.6.6 Currying
www.composingprograms.com/pages/16-higher-order-functions.html 8/12
2023/9/2 10:31 1.6 Higher-Order Functions
Some programming languages, such as Haskell, only allow functions that take a single argument, so
the programmer must curry all multi-argument procedures. In more general languages such as
Python, currying is useful when we require a function that takes in only a single argument. For
example, the map pattern applies a single-argument function to a sequence of values. In later
chapters, we will see more general examples of the map pattern, but for now, we can implement the
pattern in a function:
We can use map_to_range and curried_pow to compute the first ten powers of two, rather than
specifically writing a function to do so:
We can similarly use the same two functions to compute powers of other numbers. Currying allows us
to do so without writing a specific function for each number whose powers we wish to compute.
In the above examples, we manually performed the currying transformation on the pow function to
obtain curried_pow . Instead, we can define functions to automate currying, as well as the inverse
uncurrying transformation:
The curry2 function takes in a two-argument function f and returns a single-argument function g .
When g is applied to an argument x , it returns a single-argument function h . When h is applied to y , it
calls f(x, y) . Thus, curry2(f)(x)(y) is equivalent to f(x, y) . The uncurry2 function reverses the currying
transformation, so that uncurry2(curry2(f)) is equivalent to f .
www.composingprograms.com/pages/16-higher-order-functions.html 9/12
2023/9/2 10:31 1.6 Higher-Order Functions
unnamed functions. A lambda expression evaluates to a function that has a single return expression
as its body. Assignment and control statements are not allowed.
The result of a lambda expression is called a lambda function. It has no intrinsic name (and so Python
prints <lambda> for the name), but otherwise it behaves like any other function.
In an environment diagram, the result of a lambda expression is a function as well, named with the
greek letter λ (lambda). Our compose example can be expressed quite compactly with lambda
expressions.
x 12
Return
169
value
y 12
Return
13
value
x 13
Return
169
value
Some programmers find that using unnamed functions from lambda expressions to be shorter and
more direct. However, compound lambda expressions are notoriously illegible, despite their brevity.
The following definition is correct, but many programmers have trouble understanding it quickly.
In general, Python style prefers explicit def statements to lambda expressions, but allows them in
cases where a simple function is needed as an argument or return value.
Such stylistic rules are merely guidelines; you can program any way you wish. However, as you write
programs, think about the audience of people who might read your program one day. When you can
make your program easier to understand, you do those people a favor.
The term lambda is a historical accident resulting from the incompatibility of written mathematical
notation and the constraints of early type-setting systems.
It may seem perverse to use lambda to introduce a procedure/function. The notation goes
back to Alonzo Church, who in the 1930's started with a "hat" symbol; he wrote the square
function as "ŷ . y × y". But frustrated typographers moved the hat to the left of the parameter
and changed it to a capital lambda: "Λy . y × y"; from there the capital lambda was changed to
lowercase, and now we see "λy . y × y" in math books and (lambda (y) (* y y)) in Lisp.
www.composingprograms.com/pages/16-higher-order-functions.html 10/12
2023/9/2 10:31 1.6 Higher-Order Functions
—Peter Norvig (norvig.com/lispy2.html)
Despite their unusual etymology, lambda expressions and the corresponding formal language for
function application, the lambda calculus, are fundamental computer science concepts shared far
beyond the Python programming community. We will revisit this topic when we study the design of
interpreters in Chapter 3.
>>> @trace
d e f triple(x):
return 3 * x
In this example, A higher-order function trace is defined, which returns a function that precedes a call
to its argument with a print statement that outputs the argument. The def statement for triple has an
annotation, @trace , which affects the execution rule for def . As usual, the function triple is created.
However, the name triple is not bound to this function. Instead, the name triple is bound to the
returned function value of calling trace on the newly defined triple function. In code, this decorator is
equivalent to:
In the projects associated with this text, decorators are used for tracing, as well as selecting which
functions to call when a program is run from the command line.
Extra for experts. The decorator symbol @ may also be followed by a call expression. The expression
following @ is evaluated first (just as the name trace was evaluated above), the def statement second,
and finally the result of evaluating the decorator expression is applied to the newly defined function,
and the result is bound to the name in the def statement. A short tutorial on decorators by Ariel Ortiz
gives further examples for interested students.
Continue: 1.7 Recursive Functions
www.composingprograms.com/pages/16-higher-order-functions.html 11/12
2023/9/2 10:31 1.6 Higher-Order Functions
Composing Programs by John DeNero, based on the textbook Structure and Interpretation of Computer Programs by Harold Abelson and Gerald Jay Sussman, is licensed under a
Creative Commons Attribution-ShareAlike 3.0 Unported License.
www.composingprograms.com/pages/16-higher-order-functions.html 12/12