:::success
Agenda for today
- The most famous higher-order functions (HOF)
- `map` (transform all!)
- `filter` (keep some!)
- `fold` (combine to one value!)
- left vs right fold
- Practice:
- Writing HOFs
- Using HOFs
- If time: testing with RackUnit
:::
# Announcements
- A2 pairs out, due Thursday
- Note: make sure names match exactly, as you functions will be graded in part via unit test suites.
- Thinking about testing is an important learning goal of this course! In A3, your _tests_ will also be programmatically graded by how many _wrong_ implementations they can find.
- My help hours: next week, likely moving to:
- 3:45-5:15pm Mondays in L043 (this room)
- 3:45-5:15pm Thursdays in L037 (systems lab)
# Higher-order function terminology
First, a bit of terminology:
A **higher order function** is a function that takes one or more other functions as arguments and/or produces a function as a result.
A language having **first-class** functions means that functions can be used in any other place a value can be used. This is also sometimes referred to as "functions as values".
Why two different terms? "Higher-order functions" are a mathematical concept that applies outside of programming, where "first-class functions" is more commonly used to describe a feature of a programming language.
# `map` (transform all)
Consider the following warmup questions:
```racket
(define (double-all ls)
(if (empty? ls) '()
(cons (* 2 (first ls)) (double-all (rest ls)))))
(define (add-1-all ls)
(if (empty? ls) '()
(cons (+ 1 (first ls)) (add-1-all (rest ls)))))
(define ls '(1 2 3 4 5))
;; WARMUP QUESTIONS:
;; Q1: What is shared across these two implementations?
;; Q2: What is different?
;; Q3: Write two lambda expressions to capture what each function does to
;; each element of the list (the "different" piece, one for double-all
;; and one for add-1-all).
```
For Q1: both implementations produce a new list by applying some transformation to every element in the argument list.
For Q2: `double-all` doubles the element, `add-1-all` adds 1 to the element.
For Q3: here are the two lambdas:
```racket
(lambda (x) (* 2 x)) ; doubles
(lambda (x) (+ 1 x)) ; adds 1
```
Now, let's write a function that can do _either_ of these actions based on the function passed in as an argument: we can start with `double-all`, rename it to `out`, then take the function to apply as an argument `f`:
```racket
(define (out f ls)
(if (empty? ls) '()
(cons (f (first ls))
(out f (rest ls))))) ; note: need to pass f
```
We can then call this as:
```racket
> (out (lambda (x) (* 2 x)) ls)
'(2 4 6 8 10)
> (out (lambda (x) (+ 1 x)) ls)
'(2 3 4 5 6)
```
We have accomplished the same thing as our two original functions, without duplicating so much code!
:::success
This is in fact the **most famous** higher order function: better known by the name **map**!
**map** applies a given function to each element of a collection, producing a new collection of results in the same respective order as the original list.
:::
With its more common name:
```racket
; Most famous higher-order function!
(define (map f xs)
(if (empty? xs)
'()
(cons (f (first xs)) (map f (rest xs)))))
(map (lambda (x) (* 2 x)) ls)
(map (lambda (x) (+ 1 x)) ls)
```
Another question: based on our implementation, does the value we return for each element need to be the same type as the values in the list?
Answer: No! Nothing here requires that the output have the same type. Consider:
```racket
> (map (lambda (x) (number->string x)) ls)
'("1" "2" "3" "4" "5")
```
Question: is this good style? Can you see how we can make it more concise?
Answer: rather than using a lambda, we _already_ have a function that converts a number to a string: it's named `number->string`! That is, we don't need to introduce a lambda when we already have a binding that does what we need.
It's much better style to do:
```
> (map number->string ls)
```
:::success
:star: style point: don't use redundant lambdas! Functions are values, so you can map an existing variable directly.
:::
### Map in other languages
Map is built into _many_ languages now:
Python:
```python
# Map in python
doubled = list(map(lambda x: x * 2, numbers))
# Or: similar idea, list comprehension
doubled = [x * 2 for x in numbers]
```
JavaScript:
```javascript
const doubled = numbers.map(x => x * 2);
```
Rust:
```rust
let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect();
```
OCaml:
```ocaml
let doubled = List.map (fun x -> x * 2) numbers;;
```
## More properties of map:
:::success
Properties of **map**:
- Input items and return items do not need to be of the same type
- Preserves the length of the original list
:::
---
# `filter` (keep some)
Let's look at another case where we could use a nice functional abstraction:
```racket
(define (keep-even ls)
(if (empty? ls) '()
(let ((rs (keep-even (rest ls))))
(if (even? (first ls))
(cons (first ls) rs)
rs))))
(define (keep-gt3 ls)
(if (empty? ls) '()
(let ((rs (keep-gt3 (rest ls))))
(if (> (first ls) 3)
(cons (first ls) rs)
rs))))
```
What do both of these functions do? They both apply a check to every element of a list, producing a new list with only the values that passed the check.
In the first function, the check is `even?`. In the second function, the check is '(lambda (x) (> x 3)`.
This leads us to another famous higher-order function: **filter**!
```racket
; Another famous higher-order function.
(define (filter f xs)
(if (null? xs)
null
(if (f (first xs))
(cons (first xs) (filter f (rest xs)))
(filter f (rest xs)))))
```
Rewriting our two functions from above:
```racket
> (filter even? ls)
'(2 4)
> (filter (lambda (x) (> x 3)) ls)
'(4 5)
```
Question: what restrictions do we have on the function for filter?
There are in a sense two different answers:
1. *Conceptually*, we want it to return a boolean.
2. In practice, Racket treats anything other than `#f` as true.
For example:
```racket
> (filter (lambda (x) (+ 1 x)) ls)
'(1 2 3 4 5)
```
:::warning
Be aware of this as you are debugging! **filter** will run with functions that don't produce a boolean, but probably that was not what you meant to do.
:::
## Properties of **filter**
:::success
Properties of **filter**:
- The length of the produced list may be anything from 0 to the length of the original list.
- (In Racket) this does not mutate the list or its elements, conceptually it copies them to build a new list.
:::
---
# `fold`: (reduce/combine to result)
We'll motivate our third famous higher-order function with another pair of examples:
```racket
(define (sum ls)
(if (empty? ls)
0
(+ (first ls) (sum (rest ls)))))
(define (product ls)
(if (empty? ls)
1
(* (first ls) (product (rest ls))))
```
Question: what is **not** shared across these two functions?
This will let us know how many arguments our HOF needs (in addition to the list its operating on)?
Answer:
1. The function to apply (e.g., `+` vs `*`)
2. The base case (here, the identity: `0` vs `1`)
- we can also think of this as the "starting" value
We call this *fold* (though in other languages, its sometimes called *reduce*).
## Fold group exercise:
- Implement 2 version of fold
- 1 should use natural recursion (same pattern as sum and product)
- 1 should use tail recursion
- Discuss: do both versions produce the same result?
- It's likely helpful to draw out list of calls
- '(fold + 0 ls) ; ls '(1 2 3)`
- '(fold + (+ ? ?) (?))`
- (Followup): what happens if we pass both '(fold cons empty lst)`
Solution:
```racket
; Our third famous higher-order function: fold!
; Natural recursion
(define (foldr func acc lst)
(if (empty? lst)
acc
(func (first lst) (foldr func acc (rest lst)))))
; Note: foldr is not tail recursion, since foldr is not called
; as the outer-most expression of the "else"
; Said another way: after a recursive call returns, the function
; still needs to do the work of applying the function to the
; current element and the recursive result.
; In tail recursion, the function should have no work left by
; the time the recursive call returns
; Tail recursion
; Note we don't *need* a helper function with an additional
; acc, since we can use the argument tracking the "starting"
; value for that!
(define (foldl func acc lst)
(if (empty? lst)
acc
; Note: correction from previous version of the notes,
; acc should be the 2nd arg to the (func ...) call to
; match Racket's built-in foldl order. In class, our
; specification was not precise enough to require this,
; but the version below *is* what I showed in class as
; the reference solution!
(foldl func (func (first lst) acc) (rest lst))))
```
```racket
; Fold right recursion
(foldr + 0 '(1 2 3))
; result: (+ 1 (foldr + 0 '(2 3)))
(foldr + 0 '(2 3))
; result: (+ 2 (foldr + 0 '(3)))
(foldr + 0 '(3))
; result: (+ 3 (foldr + 0 '()))
(foldr + 0 '())
; result 0
; Overall, computes (+ 1 (+ 2 (+ 3 0))
; ^ base case, initial acc
; ^ processes elems -> to the right
; Note: acc stays the same, is just passed down to the base
; case
; Fold left recursion
(foldl + 0 '(1 2 3))
(foldl + (+ 1 0) '(2 3))
(foldl + (+ 2 1) '(3))
(foldl + (+ 3 3) ())
; returns (+ 3 3) = 6
; Overall, computes (+ 3 (+ 2 (+ 1 0)))
; ^ initial acc
; ^ processes elems <- to the left
; 3 is the outer-most elem
; Note: acc changes on each recursive call
```
:star: `foldr` uses an accumulator, but is not tail recursive!
- `foldl` is more efficient because it is tail recursive, but if order matters, it effectively visits the elements in reverse order.
- `foldr` maintains the same order. This matters for e.g., string concatenation. If stack space is more of a concern then time, it's more efficient to `foldl` and then `reverse` (both tail recursive).
# Combination HOF example
Write a function that returns the sum of the squares of all the positive numbers in a list.
```racket
; Answer using all 3 famous HOFs!
(foldl + 0 (map (lambda (x) (* x x)) (filter positive? ls)))
```
Note: this solution uses HOFs, but does not use _explicit_ recursion (it does not call a function recursively on the `rest` of `ls`, even though implicitly we know the implementations do recur). Assignment 2 asks you to do some similar examples.
# Testing
A few quick notes on [RackUnit](https://docs.racket-lang.org/rackunit/), which I encourage you to use for Assignment 2:
```racket
; Use the unit testing library RackUnit
(require rackunit)
; Our function-under-test
(define (f x) (cons x empty))
; check-equal? checks for equivalent values
(check-equal? (f 0) '(0)) ; succeeds
; check-eq? check for identical values (the same heap location)
; This matters for cons cells, but not always for literals
;(check-eq? (f 0) '(0)) ; fails, copy of cons cell
; For functions that return booleans, good style to use:
(check-true (odd? (first (f 1))))
(check-false (odd? (first (f 2))))
```
:::warning
Important note:
- Moving forward, I encourage you to use tests over commented-out interaction code!
:::