Comp 112
Lecture 13
Function Literals and Higher-Order Functions2018.04.24
In Python, we can write expressions, called literals, for:
int
s
str
ings
list
s
tuple
s
dict
s
etc.
We can also write literal expressions for functions:
This is the function that doubles its argument (here called x
)
It is equivalent to:
The terminology “lambda” is historical: it comes from the original model of universal computation, called “λ-calculus”, which was invented in the 1930s, before computers even existed.
The general form of a lambda expression is:
One of the most fundamental operations on functions is function composition: using the output from one function as the input for another.
double = lambda x : 2 * x # signature: function (int -> int)
succ = lambda x : x + 1 # signature: function (int -> int)
We can write the function composition operation itself as a higher-order function:
def compose (f , g) :
# signature: any a , b , c . function (a -> b) , function (b -> c) -> function (a -> c)
return lambda x : g (f (x))
Letting us write:
Now we can compose functions without using def
or lambda
.
The operation of integer addition has a neutral element:
The operation of integer multiplication has a neutral element:
The operation of list concatenation has a neutral element:
Likewise, the operation of function composition has a neutral element, called the identity function:
This function is pretty boring, but very useful for building higher-order functions.
Using induction, identity
and compose
we can define more complex patterns of function composition:
def iterate (n , f) :
# signature: any a . int , function (a -> a) -> function (a -> a)
# precondition: n >= 0
# composes f with itself n times
if n == 0 :
return identity
elif n > 0 :
return compose (f , iterate (n - 1 , f))
We can turn a function that expects two arguments into a function that expects the first argument and returns a function expecting the next one:
# signature: function (int , int -> int)
mult = lambda x , y : x * y
# signature: function (int -> function (int -> int))
mult_c = lambda x : lambda y : mult (x , y)
triple = mult_c (3) # = lambda y : mult (3 , y)
Instead of doing this by hand, we can write a higher-order function to do this for any two-argument function. This is called function currying:
# signature: any a , b , c . function (function (a , b -> c) -> function (a -> function (b -> c)))
curry = lambda f : lambda x : lambda y : f (x , y)
mult_c = curry (mult)
Aside: using induction, we can curry functions with any number of arguments (although this is a bit tricky).
We can curry higher-order functions like map
and filter
:
# signature : any a , b . function (function (a -> b) -> function (iterator (a) -> iterator (b)))
map_c = curry (map)
# signature : any a . function (function (a -> bool) -> function (iterator (a) -> iterator (a)))
filter_c = curry (filter)
This makes them even more useful because then we can partially apply them to easily form new functions:
With higher-order functions we can write useful bits of programs with no loops and no recursion.
The ability to manipulate functions just like any other kind of data greatly expands our power of expression.
Recall the homework problem of finding the average weight of rats on a given diet:
data = \
[ # name , diet , weight
('Whiskers' , 'rat chow' , 300.0) ,
('Mr. Squeeky' , 'swiss cheese' , 450.0) ,
('Pinky' , 'rat chow' , 320.0) ,
('Fluffball' , 'swiss cheese' , 500.0)
]
Here is a high-level strategy:
def avg_weight (diet , table) :
matching_weights = compose_all ([filter_c (lambda row : row [1] == diet) , map_c (lambda row : row [2]) , list]) (table)
return sum (matching_weights) / len (matching_weights) if matching_weights != [] else 0.0
Higher-order functions let us work at higher levels of conceptual abstraction so that we can write complex programs concisely, with minimal bureaucratic overhead.
In the accumulator patten for processing a list:
we initialize an accumulator,
then we iterate over the elements of a list using a loop,
in the loop body, we update the accumulator based on its current value and the current list element
then we return or further process the accumulator after the loop
We can turn the accumulator pattern into a higher-order function for processing lists:
def reduce (f , init , xs) :
# signature: any a , b . function (b , a -> b) , b , list (a) -> b
acc = init
for x in xs :
acc = f (acc , x)
return acc
Conceptually, the reduce function is doing this:
As with map
and filter
, reduce
becomes even more useful when we curry it:
Now we can do list reductions with no loops and no recursion, often as one-liners:
# the sum of the numbers in a list (or 0 if empty):
sum = reduce_c (lambda acc , x : acc + x) (0)
sum ([1 , 2 , 3 , 4 , 5])
# the longest string in a list (or '' if empty):
longest_string = reduce_c (lambda acc , x : acc if len (acc) >= len (x) else x) ('')
longest_string (['was' , 'it' , 'the' , 'best' , 'of' , 'times' , 'or' , 'not' , '?'])
# the largest number in a non-empty list:
max = lambda xs : reduce_c (lambda acc , x : acc if acc >= x else x) (xs [0]) (xs [1 : ])
max ([1 , 2 , 3 , 4 , 3 , 2 , 1])
This style of programming takes a little getting used to, but is very powerful.
Functions are the foundations of programming. I encourage you to be curious about how you can use them to express your ideas as programs.
No Reading assignment.
This week in lab you will be working on your projects, so bring your questions!
No homework, other than to work on your project and study for the last midterm.