COMP 210 Lab 8: Abstract Functions
There are many more exercises here than you can do in lab.
Do a representative subset.
Review
In class, we were introduced to some abstract functions:
map
, foldr
, and foldr
.
Three three functions are "abstract" in the sense that they
form the common cores of many typical functions on lists.
In other words, they provide invariant core of computations, taking
other functions as arguments which represent the variant parts of
computations.
This lab has exercises on these three abstract functions
and asks you to define some additional abstract functions.
The current homework introduces another common abstract function on lists,
filter
.
Summary of map
, foldr
, foldl
map |
; map : (any1 -> any2) list-of-any1 -> list-of-any2
(define (map f l)
(cond [(empty? l) empty]
[(cons? l) (cons (f (first l))
(map f (rest l)))]))
|
(map f (list x1 x2 ... xn-1 xn))
=
(list (f x1) (f x2) ... (f xn-1) (f xn))
|
foldr |
; foldr : (any1 any2 -> any2) any2 list-of-any1 -> any2
(define (foldr f base l)
(cond [(empty? l) base]
[(cons? l) (f (first l)
(foldr f
base
(rest l)))]))
|
(foldr f base (list x1 x2 ... xn-1 xn))
=
(f x1 (f x2 (... (f xn-1 (f xn base)))))
|
foldl |
; foldl : (any1 any2 -> any2) any2 list-of-any1 -> any2
(define (foldl f base l)
(cond [(empty? l) base]
[(cons? l) (foldl f
(f (first l) base)
(rest l))]))
|
(foldl f base l)
=
(f xn (f xn-1 (... (f x2 (f x1 base)))))
|
Abstract functions vs. templates and recipes
Using abstract functions often results in one-line non-recursive
functions representing the variant part of the computation.
The recursion is hidden inside the invariant abstract function.
Thus, using the abstract functions often replaces the standard recursive
templates.
In the short term, while you are still getting used to abstract functions,
we strongly recommend that you first follow the design recipe to
produce the familiar recursive solutions. Then
go back and edit your code to use abstract functions where applicable.
In the long term, you will learn to identify common patterns before you
actually write code and be able to go directly to writing the shorter
versions using abstract functions.
You will probably find map
and filter
the easier
to use at first. foldr
and foldl
are more general
and take more practice to get comfortable with.
You will get practice on homeworks and the next two exams.
Where do we get those contracts?
In class, we briefly looked at the contract for foldr
.
Let's review that discussion, adding some details to understand
one possible line of thought of obtaining the contract.
foldr
takes a function of two arguments, a value for the
base case, and a list, and it returns something:
foldr : (?? ?? -> ??) ?? list-of-?? -> ??
foldr
doesn't care what kind of things are in the list,
nor of the kinds of things its function argument takes or returns,
nor what the base case value is:
foldr : (any any -> any) any list-of-any -> any
foldr
's return value can come from two places:
the base case value and the return value of
foldr
's function argument.
So, each of these must be the same kind of thing.
Let's annotate the contract to clarify this sameness:
foldr : (any any -> any2) any2 list-of-any -> any2
Furthermore, the second argument of foldr
's function argument
has to be the same kind of thing, since that's where the recursive call is
located:
foldr : (any any2 -> any2) any2 list-of-any -> any2
Also, the input list elements are used as the first argument to
foldr
's function argument:
foldr : (any1 any2 -> any2) any2 list-of-any1 -> any2
But, the input list elements need not be the same kind of thing as the
eventual result, even though in many
examples they are. E.g., foldr
can be used to define the
length of a list of symbols.
foldr : (any2 any2 -> any2) any2 list-of-any2 -> any2
If you're a mathematician, and thus like Greek letters, you may prefer
a more traditional form:
foldr : (α β -> β) β list-of-α -> β
Contract exercise
Derive the given contracts for map and foldl
in the same way.
|
An aside
That line of reasoning is very close to the steps a
type inference system
would go through to assign such a contract.
In later courses, it will be important to understand the related ideas of
type checking and type inference.
|
foldr
vs. foldl
foldr
embodies reverse accumulation on lists.
It first applies the given function to the rightmost elements
of the given list. E.g., to sum (list 7 3 -12 2 9)
,
7 3 -12 2 9
+0
=9
+9
=11
+11
=-1
+-1
=2
+2
=9
foldl
embodies forward accumulation on lists.
It first applies the given function to the leftmost elements
of the given list.
7 3 -12 2 9
+0
=7
+7
=10
+10
=-2
+-2
=0
+0
=9
As we've seen before, these are sometimes essentially the same thing.
In particular, given a function that is both associative and commutative,
they give the same result.
Reminder:
A function f is associative if
(f (f x y) z)
=
(f x (f y x)).
A function f is commutative if
(f x y)
=
(f y x).
Exercises
map
, foldr
,
and foldl
exercises
-
Develop add-2-to-each using map :
(add-2-to-each (list 3 8 -1)) returns (list 5 10 1)
-
Develop add-n-to-each using map :
(add-n-to-each 5 (list 3 8 -1)) returns (list 8 13 4)
-
Try these as samples of when
foldr and foldl behave the same.
These should come as no surprise, since we've previously written
these functions each with reverse and forward accumulation.
(foldr + 0 (list 7 3 -12 2 9))
(foldl + 0 (list 7 3 -12 2 9))
(foldr * 1 (list 7 3 -12 2 9))
(foldl * 1 (list 7 3 -12 2 9))
-
Using foldr or foldl , define a function
to compute whether all elements in a list of booleans are true.
(This is commonly called andmap .)
(Note: and is not a function, but actually a special
piece of syntax, since it doesn't always evaluate its second
argument! You'll need to wrap it inside a lambda .)
-
Using foldr or foldl , define a function
to compute whether any elements in a list of booleans are true.
(This is commonly called ormap .)
-
Using foldr or foldl ,
write a function which takes a list of anything
and returns the length of the list.
(As we've already seen, this is commonly called length .)
-
For the curious (since we've barely introduced
visitors)...
Repeat those last three exercises using the visitor-style
version of foldr :
;; A Visitor is a structure made up of two functions
;; One for the base case and
;; one for the inductive case
(define-struct Visitor (fBase fInduct))
;; execute: list-of-any1 Visitor any2 --> any3
;; Executes ("accepts") the visitor on the list
;; returning the result.
;; param is passed to the visitor unmodified.
(define (execute a-list visitor param)
(cond
[(empty? a-list) ((Visitor-fBase visitor) a-list param)]
[(cons? a-list) ((Visitor-fInduct visitor) a-list param)]))
;; Structure used for both foldrList and foldlList
;; f is the inductive case function
;; base is the base case value, of type any2
(define-struct FoldInp (f base))
;; foldrVisitor implements foldr as a visitor
(define foldrVisitor
(make-Visitor
;; empty FoldInp --> any2
;; returns the base case value
(lambda (a-list foldInp)
(FoldInp-base foldInp))
;; non-empty-list-of-any1 FoldInp --> any2
;; applies the inductive case function to first and
;; the recursive result and returns the result.
(lambda (a-list foldInp)
((FoldInp-f foldInp) (first a-list) (execute (rest a-list) foldrVisitor foldInp)))))
-
Try these as a sample of when they don't.
First, figure out what they should do, and then use DrScheme
to confirm.
(foldr / 1 (list 7 3 -12 2 9))
(foldl / 1 (list 7 3 -12 2 9))
(foldr - 0 (list 7 3 -12 2 9))
(foldl - 0 (list 7 3 -12 2 9))
(foldr cons empty (list 7 3 -12 2 9))
(foldl cons empty (list 7 3 -12 2 9))
(foldr append empty (list 7 3 -12 2 9))
(foldl append empty (list 7 3 -12 2 9))
-
Develop dot-product ,
which takes in two lists of n numbers
(a1 a2 ... an) and
(b1 b2 ... bn) and returns
a1×b1 +
a2×b2 +
an×bn,
for any n≥0.
(What are some trivial test cases?)
Hint: The built-in map actually can take
in a binary function and two lists of arguments
(or a ternary function and three lists, etc.).
-
A previous lab described the prefix-sums
function:
(prefix-sums (list 8 7 3)) returns (list 8 15 18)
(prefix-sums (list 1 8 7 3)) returns (list 1 9 16 19)
(prefix-sums (list 1 1 1 1)) returns (list 1 2 3 4)
One way of computing this fits very nicely with abstract functions.
From the first two example uses, you should see that the previous
add-n-to-each is useful.
Finish the function with foldr or foldl ,
as appropriate.
|
Other abstract functions
These are certainly not the only abstract functions,
but map
, filter
, foldr
, and
foldl
have proven to be particularly useful because they
correspond to common forms of computations on lists.
Because Scheme has lists built in and these functions are so useful,
Scheme has them predefined. But the same ideas are also useful on other
data structures.
First, do the following exercises in our old non-visitor style.
Then, repeat, but using visitors.
-
Consider the idea of iterating a function:
for instance, if I repeatedly double 3,
I get 6, then 12, then 24, etc.
That is, I take the output of a function,
and again apply the function to that.
Iterating square twice, starting with 5,
yields (square (square 5)) = 625 .
If I had a function
remove-max: list -> list ,
then iterating that three times starting with
(list 7 18 2 9 3)
means
(remove-max (remove-max (remove-max (list 7 18 2 9 3))))
=
(list 2 3)
In general, iterating some function f three times
starting with x means
(f (f (f x))) .
Develop the function iterate ,
which takes a function f , a natural number n ,
and an initial input to the function x ,
and iterates the function n times.
Don't forget to determine its general contract.
-
For the curious...
Develop a second version of iterate .
Between the two versions, one should be
tail-recursive, while the other isn't.
-
Using iterate , develop remove-first-n ,
which takes a number n and list,
and returns the list with the first n items removed.
(Of course, an error would result if the list
had fewer than n items.)
-
Develop a folding function for binary trees.
; A binary tree is either
; - (make-EmptyTree)
; - (make-NETree n left right),
; where n is anything, and left and right are binary trees.
(define-struct EmptyTree ())
(define-struct NETree (n left right))
; The singleton empty tree.
(define emptytree (make-EmptyTree))
It takes a ternary (i.e., 3-input) function and
a base value.
Don't forget to determine its general contract.
(fold + 0 (make-NETree 5 (make-NETree 6 emptytree emptytree)
(make-NETree 7 emptytree emptytree)))
=
18
-
Using your folding function on binary trees, define a function
which computes the height of a tree.
This is very similar to computing the length of a list.
(height (make-NETree 5 (make-NETree 6 emptytree emptytree)
(make-NETree 7 emptytree emptytree)))
=
2
(height emptytree)
=
0
|