M16: Functional Abstraction

Hint: If possible, make your browser wide enough to place the slides and commentary side-by-side.

The slides on this page are not accessible (for screen readers, etc). The 1up and 3up slides are better.

Module Topics

  1. Higher order functions
  2. Foldr
  3. Foldl
  4. Build-list
Slide 000
Slide 001
Slide 002
Slide 003

Video: Download m16.40_map

video m16.40_map
Slide 004

Take negate-list and compute-tax. Rename the function as my-map and rename the pay parameter as lst. The results are identical except for the function applied to (first lst).

Make that function a parameter.

That is, the underlined portions of my-map are all the changes required to account for the differences between negate-list and compute-taxes, other than renaming parameters.

Slide 005

my-map bears a lot of similarity to the listof-X-template. They are based on the same data definition, after all.

(define (listof-X-template lox)
  (cond [(empty? lox) (...)]
        [else         (... (first lox)
                           (listof-X-template (rest lox)))]))

The template, is more general and so leaves out some things that map includes. But both check for the empty list. Both recurse on the rest of the list. Both do something to the first thing on the list.

Slide 006
Slide 007
Slide 008

Video: Download m16.45_map_contract

video m16.45_map_contract
Slide 009

As noted on the slide, Racket provides many built-in higher order functions. CS135 will discuss five of them: filter, map, foldr, foldl, and build-list. These are the only higher order functions you need for CS135. Feel free (but not obligated) to explore the rest, but don’t use them on assignments or exams unless we explicitly point you to them.

One difference between my-map and the built-in map is that the built-in version can consume multiple lists. The function it consumes must have the same number of parameters as there are lists. For example,

(map < (list 5 10 20) (list 3 12 18))  (list false true false)
Slide 010
Slide 011
Slide 012

(Note: CS135 used to use the term “Abstract List Function” or “ALF” where we now use “Higher Order Function” or “HOF”. This video still uses the older vocabulary.)

Video: Download m16.50_foldr

video m16.50_foldr
Slide 013

As we did for my-map, let’s harmonize the identifiers we have control over – the function names and parameters – and use underlines to show where the differences are. That gives us this:

(define (my-foldr lst)
  (cond [(empty? lst) ____]
        [else        (____ (first lst)
                           (my-foldr (rest lst)))])
)

This suggests that we need to pass two arguments to my-foldr – one to go into the first blank and one to go into the second.

Slide 014
Slide 015
Slide 016

combine: a function that combines two values, the first thing on the list and the result of processing the rest of the list.

base: a value to provide when we hit the base case.

Slide 017
Slide 018
Slide 019
Slide 020

The contract for foldr is the most complicated contract in CS135.

Video: Download m16.60_foldr_contract

video m16.60_foldr_contract
Slide 021

If we were to trace sum-of-numbers, the trace would be pretty short. foldr is built-in. Like other built-in functions, its application results in a single step.

(sum-of-numbers (list 1 2 3 4))
 (foldr + 0 (list 1 2 3 4))
 10
Slide 022

Note that count-symbols does not use (first lst). Does that “break” foldr?

Slide 023

You may notice that many of our examples use rror as the name of the second parameter of the lambda expression. That’s to remind us which parameter is which. In this case, x is the first value on the list and rror is the Result of Recursing On the Rest of the list (the part that follows x).

Slide 024

Slide 025
Slide 026
Slide 027

Here’s the code for negate-list and one of the functions we used to derive foldr:

(define (negate-list   lst)
  (cond [(empty? lst) empty]
        [else         (cons (- (first lst)) 
                            (negate-list (rest lst)))]))

define (sum-of-numbers lst)
  (cond [(empty? lst) 0]
        [else         (+ (first lst) 
                         (sum-of-numbers (rest lst)))]))

Notice the similar structure! We could have used negate-list as one of our examples to derive foldr. The combining function for negate-list, however, is a bit more complicated. See the explanation on the slide.

Slide 028

We’ll start to use the terminology not explicitly recursive to mean a function where recursion is done via a higher order function such as foldr.

Slide 029
Slide 030

Implementing my-filter using foldr takes a bit more work.

One approach (presented here) is to try to make this code match the structure of foldr so we can identify the values to pass to foldr as the base and combining function. Another approach (the video) is to construct the needed pieces from what we know about the problem and the contracts for my-filter and foldr.

Here’s the code for my-filter. Unfortunately, it does not look much like the code for foldr.

(define (my-filter pred? lst)
  (cond [(empty? lst) empty]
        [(pred? (first lst))
         (cons (first lst) (my-filter pred? (rest lst)))]
        [else (my-filter pred? (rest lst))]))

(define (my-foldr combine base lst)
  (cond [(empty? lst) base]
        [else (combine (first lst)
                       (my-foldr combine base (rest lst)))]))

To make these two functions have the same structure, we can start by moving the last two question/answer clauses into an else clause:

(define (my-filter pred? lst)
  (cond [(empty? lst) empty]
        [else (cond [(pred? (first lst))
                     (cons (first lst) (my-filter pred? (rest lst)))]
                    [else              (my-filter pred? (rest lst))])]))

Next, make the contents of the else clause into a function:

(define (my-filter pred? lst)
  (local [(define (maybe-cons x filtered)
            (cond [(pred? x) (cons x filtered)]
                  [else filtered]))]
    (cond [(empty? lst) empty]
          [else (maybe-cons (first lst)
                            (my-filter pred? (rest lst)))])))

The last three lines of this code matches foldr. It now becomes clear that we could use foldr by passing empty to base and the function maybe-cons to the combining function. Furthermore, we see that maybe-cons is a function that’s only used once and can therefore be replaced with a lambda expression. That results in the following implementation using foldr:

(define (my-filter pred? lst)
  (foldr (lambda (x filtered)
           (cond [(pred? x) (cons x filtered)]
                 [else filtered]))
         empty lst))

An alternative approach to developing this is given in the video.

Video: Download m16.70_filter

video m16.70_filter
Slide 031
Slide 032
Slide 033
Slide 034
Slide 035

In spite of the comment about how higher order functions should be used, we do ask some “brain-bendy” questions to get you to think deeply about how they work.

Slide 036
Slide 037

Harmonizing names, as we did before, results in the following:

(define (my-foldl lst0)
  (local [(define (foldl/acc lst acc)
            (cond [(empty? lst) acc]
                  [else (foldl/acc (rest lst)
                                   (____ (first lst) acc))]))]
    (my-foldl/acc lst0 ____)))

This suggests that we need two additional parameters.

Slide 038
Slide 039

Note the similarity to foldr:

  • Both consume a combining function.
  • Both consume a base value.
  • Both consume a list.
Slide 040

Intuitively, the effect of the application (foldl f b (list x_1 ... x_n-1 x_n)) is to compute the value of the expression (f x_n (f x_n-1 (... (f x_1 b)))).

We can see that by doing a trace, similar to slide 16 :

(my-foldl f 0 (list 3 6 5))
 (foldl/acc (list 3 6 5) 0)
 (foldl/acc (list 6 5) (f 3 0))
 (foldl/acc (list 5) (f 6 (f 3 0)))
 (foldl/acc (list ) (f 5 (f 6 (f 3 0))))
 (f 5 (f 6 (f 3 0)))
Slide 041

foldr and foldl may give the same results; they may be different. For example,

(foldr + 0 (list 1 2 3 4))  10
(foldl + 0 (list 1 2 3 4))  10

but

(foldr string-append "2B" (list "To" "Be" "Or" "Not")) 
 "ToBeOrNot2B"
(foldl string-append "2B" (list "To" "Be" "Or" "Not")) 
 "NotOrBeTo2B"

In general, if the combining operation is commutative foldr and foldl will generate the same result. In these examples, + is commutative but string-append is not.

Aside:

This section is for those who are interested; it is not official CS135 content.

The previous discussion may make you think that in many circumstances the choice between foldr and foldl doesn’t matter. Here is another consideration.

Consider tracing foldr and foldl:

(foldr + 0 (list 1 2 3 4))
 (+ 1 (foldr (list 2 3 4)))  ; omitting + and 0, since they don't change
 (+ 1 (+ 2 (foldr (list 3 4))))
 (+ 1 (+ 2 (+ 3 (foldr (list 4)))))
 (+ 1 (+ 2 (+ 3 (+ 4 foldr empty))))
 (+ 1 (+ 2 (+ 3 (+ 4 0))) ⇒* 10

(foldl + 0 (list 1 2 3 4))
 (f/acc (list 1 2 3 4) 0)
 (f/acc (list 2 3 4) 1)
 (f/acc (list 3 4) 3)
 (f/acc (list 4) 6)
 (f/acc empty 10) ⇒* 10

Notice that the longest step for foldr has as many applications of + as there are numbers in the original list. If we were summing a list with 1,000 numbers, the longest step would have 1,000 pending applications.

Each of those pending applications consumes memory in the computer. If there are too many of them, the computer may run out of memory and be unable to execute the program.

The trace for foldl, on the other hand, never gets longer than a single application of f/acc. Even if the original list had 1,000 numbers, the number of pending applications does not grow and thus does not consume additional memory.

This is an example of tail recursion. Tail recursion is a good thing because it allows for computations to be more thoroughly optimized, especially in terms of the memory consumed by the computation but also in terms of the time it takes.

Slide 042
Slide 043

What is the contract for foldl?

It’s a contract, so it will look like ______ ______ ______ -> ______.

From the trace we did earlier,

(foldl f b (list x_1 ... x_n-1 x_n))
 (f x_n (f x_n-1 (... (f x_1 b))))

we can see that the third parameter is a list of something, say X. Furthermore, we can see from the trace that the combining function consumes two parameters, the first one being an X from the list:

(X ___ -> ___) _____ (listof X) -> _____

The combining function, f, consumes both b and the result of f for its second parameter, suggesting all three of these are the same type. There is no indication that it must be an X so we’ll introduce Y as a new type.

(X Y -> Y) Y (listof X) -> _____

Finally, the result of foldl is the same as the result of the combining function, giving a final contract that is the same as foldr:

(X Y -> Y) Y (listof X) -> Y

Slide 044

Like map, foldr and foldl can handle multiple lists provided the combining function has one parameter for each list plus the “rror” parameter.
For example,

(foldr + 0 (list 1 2 3) (list 4 5 6)) 
 21
(foldr (lambda (x y rror)
         (cons (+ x y) rror))
       empty (list 1 2 3) (list 4 5 6))
 (list 5 7 9)
(foldl (lambda (x y z rror)
         (cons (+ x y z) rror))
       empty (list 1 2 3) (list 4 5 6) (list 7 8 9))
 (list 18 15 12)
Slide 045
Slide 046

What’s the contract for my-build-list?

We see from examples and the code that it consumes two parameters. The second is a function consuming one argument. We also see from the example and the presence of cons and empty that it produces a list. That gives a partial contract of:

____ (____ -> ____) -> (listof X)

The i in list-from starts at 0 and is only incremented by 1, making it a Nat. The only use of n is comparing to that Nat, so it may as well be a Nat as well. The combining function, f, consumes i (a Nat). Whatever f produces is what goes into the list, which we already designated with X.

That gives a final contract of Nat (Nat -> X) -> (listof X).

Slide 047
Slide 048

foldr and build-list are both built in, so we don’t trace them.

We split the summation into two parts, each handled by a higher order function. The first part is a list of f applied to each of the numbers in 0..n-1; that’s handled by build-list. The second part is summing them up; that’s handled by foldr.

Slide 049
Slide 050

Video: Download m16.80_mult

video m16.80_mult
Slide 051
Slide 052
Slide 053