Folding is Quite General¶
The fold
function is surprisingly general, and can be used to write many
other functions. As a reminder, here is the fold
function we’re using:
(define fold
(lambda (fn empty-val lst)
(cond
((null? lst) ;; base case
empty-val)
(else ;; recursive case
(fn (car lst) (fold fn empty-val (cdr lst))))
)
)
)
This is a right fold because it starts combining elements at the right end of the list and works towards the left ed.
Mapping¶
If lst = (a1 a2 ... an)
, then (map f lst)
returns ((f a1) (f a2) ...
(f an))
, i.e. f
is applied to every element of lst
.
While map
is a standard function built-into Scheme, it is instructive to
implement our own version.
First, here is an implementation that uses recursion in a straightforward way:
(define my-map-basic
(lambda (fn lst)
(cond
((null? lst)
'())
(else
(cons (fn (car lst))
(my-map-basic fn (cdr lst)))
)
)
)
)
We can also implement map using our right fold
function like this:
(define my-map-fold
(lambda (fn lst)
(fold (lambda (a rest) (cons (fn a) rest))
()
lst)
)
)
This version consists of a single call to fold
, and has no recursive
calls. The trick is in the function passed to fold
:
(lambda (a rest) (cons (fn a) rest))
Like all functions passed to fold
, it takes two inputs. The first input is
the next item of lst
to process, and the second input is the part of the
list that has already had fn
applied to it. The function applies fn
to
a
, and then uses cons
to put it onto the start of the other already-
processed list.
How might you come up with this particular implementation? One way is to
consider the consed-out form of a list. For example, we can write (1 2 3)
like this:
(cons 1 (cons 2 (cons 3 ())))
The call (fold f empty-val lst)
is this:
(f 1 (f 2 (f 3 empty-val)))
So fold
can be seen as replacing each occurrence of cons
with f
,
and the ()
at the end with empty-val
. That means (fold cons ()
lst)
returns lst
without any change. But for map
, we do want to make
a change: we want to first apply f
to the element before consing it onto
the list. So intead of cons
, we use the function (lambda (a rest) (cons
(f a) rest))
.
Filtering¶
We can also implement the standard scheme filter
function. The call
(filter fn? lst)
returns a list that is the same as lst
except all
elements x
in lst
that cause (fn? x)
to return true are removed.
Our version will be named keep-if
. Here is a basic recursive
implementation:
(define keep-if
(lambda (fn? lst)
(cond
((null? lst)
'())
((fn? (car lst))
(cons (car lst)
(keep-if fn? (cdr lst))))
(else
(keep-if fn? (cdr lst)))
)
)
)
And now here’s a version that uses a single call to fold
and no
recursion:
(define keep-if-fold
(lambda (fn? lst)
(fold (lambda (a rest)
(if (fn? a)
(cons a rest)
rest)
)
()
lst
)
)
)
As with my-map-fold
, the trick is in the function passed to fold
. It
takes two inputs: a
, the next item to be processed, and rest
, the rest
of the already-processed list. So the function calls (fn? a)
to see if
a
satisfies fn?
. If it does, then a
is consed on to the rest of
the list; if it doesn’t, then the rest of the list is returned without change,
which effectively removes a
.
It’s also interesting to note that the code inside the function passed to
fold
is a little simpler than the corresponding code in the recursive
keep-if
. Instead of (car lst)
and (cdr lst)
we write a
and
rest
.
Exercise¶
Write of version of the standard
length
function that usesfold
, and no recursion, to calculate the length of a list.Consider the
(double-up lst)
function, which repeats each element of a list:> (double-up '(1 2 a cat)) (1 1 2 2 a a cat cat)
Impement two versions ofdouble-up
: one that usescond
(orif
) and recursion, and another that uses a single call tofold
and no recursion.