Comp 112

Lecture 12

Higher-Order Functions

2017.11.21

In Python, we can write expressions, called **literals**, for:

`int`

s`42`

`str`

ings`'hi'`

`list`

s`[1 , 2 , 3]`

`tuple`

s`(42 , 'hi')`

etc.

We can also write literal expressions for functions:

`double = lambda x : 2 * x`

This is the function that doubles its argument (here called `x`

)

$x ⟼ 2 * x$

It is equivalent to:

```
def double (x) :
return 2 * x
```

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:

`lambda <parameter_variables> : <expression_using_those_parameter_variables>`

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
succ = lambda x : x + 1
```

```
two_x_plus_one = lambda x : succ (double (x))
four_x = lambda x : double (double (x))
```

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:

```
two_x_plus_one = compose (double , succ)
four_x = compose (double , double)
```

The operation of integer addition has an identity element:

`0 + 42 == 42 == 42 + 0`

The operation of integer multiplication has an identity element:

`1 * 42 == 42 == 42 * 1`

The operation of list concatenation has an identity element:

`[] + [1 , 2 , 3] == [1 , 2 , 3] == [1 , 2 , 3] + []`

Likewise, the operation of function composition has an identity element, called the

**identity function**:`def identity (x) : # signature: any a . a -> a return x`

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 a function with itself a given number of times
if n == 0 :
return identity
elif n > 0 :
return compose (f , iterate (n - 1 , f))
```

`eight_times_x = iterate (3 , double)`

```
def compose_all (fs) :
# signature: list (function) -> function
# composes together all the functions in the list
# note: our signature language is not rich enough to describe the condition that
# the output type of each function must equal the input type of the next one.
if fs == [] :
return identity
else :
return compose (fs [0] , compose_all (fs [1 : ]))
```

`four_x_plus_three = compose_all ([double , succ , double , succ])`

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)
```

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 (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).

```
def curry_n (n , f) :
def curry_n_h (n , f , args) :
return f (* args) if n == 0 else lambda x : curry_n_h (n - 1 , f , args + [x])
return curry_n_h (n , f , [])
```

Higher-order functions like `map`

and `filter`

become even more useful when we curry them:

```
# 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)
```

because then we can partially apply them to easily form useful new functions:

```
double_all = map_c (double)
list (double_all ([1 , 2 , 3 , 4 , 5]))
```

```
just_evens = filter_c (lambda x : x % 2 == 0)
list (just_evens ([1 , 2 , 3 , 4 , 5]))
```

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:

- Identify the table rows matching the chosen diet. (filtering)
- Extract the weights from just those rows. (mapping)
- Take the average of just those weights. (arithmetic)

```
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
```

This function contains no loops and no recursion. 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

finally, we return or further process the accumulator after the loop

```
acc = init # accumulator initial value
for x in xs :
acc = f (acc , x) # accumulator update function
return acc
```

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 # accumulator initial value
for x in xs :
acc = f (acc , x) # accumulator update function
return acc
```

Conceptually, the **reduce** function is doing this:

As with `map`

and `filter`

, `reduce`

becomes even more useful when we curry it:

`reduce_c = curry_n (3 , reduce)`

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])
```

```
# the curried map function:
map_c_red = lambda f : reduce_c (lambda ys , x : ys + [f (x)]) ([])
map_c_red (double) ([1 , 2 , 3])
```

```
# the curried filter function:
filter_c_red = lambda p : reduce_c (lambda xs , x : xs + [x] if p (x) else xs) ([])
filter_c_red (lambda x : x % 2 == 0) ([1 , 2 , 3 , 4 , 5])
```

```
# average rat weight:
def avg_weight_red (diet , table) :
init = (0.0 , 0) # (sum of matching rat weights , count of matching rats)
f = lambda acc , row : (acc [0] + row [2] , acc [1] + 1) if row [1] == diet else acc
(total_weight , total_count) = reduce_c (f) (init) (table)
return 0.0 if total_count == 0 else total_weight / total_count
```

No Reading assignment.

No lab on Thursday.

No homework 12.

Study for midterm 2 (Nov. 30).

Get started on projects.

Happy Thanksgiving.