480 lines
16 KiB
Racket
480 lines
16 KiB
Racket
#reader(lib "docreader.ss" "scribble")
|
|
@require[(lib "manual.ss" "scribble")]
|
|
@require[(lib "eval.ss" "scribble")]
|
|
@require[(lib "bnf.ss" "scribble")]
|
|
@require["guide-utils.ss"]
|
|
|
|
@interaction-eval[(require (lib "list.ss"))]
|
|
@interaction-eval[(require (lib "for.ss"))]
|
|
@define[step @elem{=}]
|
|
|
|
@title{Lists, Iteration, and Recursion}
|
|
|
|
Scheme is a dialect of the language Lisp, whose name originally stood
|
|
for ``LISt Processor.'' The built-in list datatype remains a prominent
|
|
feature of the language.
|
|
|
|
The @scheme[list] procedure takes any number of values and returns
|
|
a list containing the values:
|
|
|
|
@interaction[(list "red" "green" "blue")
|
|
(list 1 2 3 4 5)]
|
|
|
|
As you can see, a list result prints in the REPL as a pair of
|
|
parentheses wrapped around the printed form of the list
|
|
elements. There's an opportunity for confusion here, because
|
|
parentheses are used for both expressions, such as @scheme[(list "red"
|
|
"green" "blue")], and printed results, such as @schemeresult[("red"
|
|
"green" "blue")]. Rememeber that, in the documentation and in
|
|
DrScheme, parentheses for results are printed in blue, whereas
|
|
parentheses for expressions are brown.
|
|
|
|
Many predefined procedures operate on lists. Here are a few examples:
|
|
|
|
@interaction[
|
|
(code:line (length (list "a" "b" "c")) (code:comment #, @t{count the elements}))
|
|
(code:line (list-ref (list "a" "b" "c") 0) (code:comment #, @t{extract by position}))
|
|
(list-ref (list "a" "b" "c") 1)
|
|
(code:line (append (list "a" "b") (list "c")) (code:comment #, @t{combine lists}))
|
|
(code:line (reverse (list "a" "b" "c")) (code:comment #, @t{reverse order}))
|
|
(code:line (member "d" (list "a" "b" "c")) (code:comment #, @t{check for an element}))
|
|
]
|
|
|
|
@;------------------------------------------------------------------------
|
|
@section{Predefined List Loops}
|
|
|
|
In addition to simple operations like @scheme[append], Scheme includes
|
|
procedures that iterate over the elements of a list. These iteration
|
|
procedures play much the same role as @tt{for} in Java and other
|
|
languages. The body of a Scheme iteration is packaged into a procedure
|
|
to be applied to each element, so the @scheme[lambda] form becomes
|
|
particularly handy in combination with iteration procedures.
|
|
|
|
Different list-iteration procedures combine iteration results in
|
|
different ways. The @scheme[map] procedure uses the per-element
|
|
results to create a new list:
|
|
|
|
@interaction[
|
|
(map sqrt (list 1 4 9 16))
|
|
(map (lambda (i)
|
|
(string-append i "!"))
|
|
(list "peanuts" "popcorn" "crackerjack"))
|
|
]
|
|
|
|
The @scheme[andmap] and @scheme[ormap] procedures combine the results
|
|
by @scheme[and]ing or @scheme[or]ing:
|
|
|
|
@interaction[
|
|
(andmap string? (list "a" "b" "c"))
|
|
(andmap string? (list "a" "b" 6))
|
|
(ormap number? (list "a" "b" 6))
|
|
]
|
|
|
|
The @scheme[filter] procedure keeps elements for which the body result
|
|
is true, and discards elements for which it is @scheme[#f]:
|
|
|
|
@interaction[
|
|
(filter string? (list "a" "b" 6))
|
|
(filter positive? (list 1 -2 6 7 0))
|
|
]
|
|
|
|
The @scheme[map], @scheme[andmap], @scheme[ormap], and @scheme[filter]
|
|
procedures can all handle multiple lists, instead of just a single
|
|
list. The lists must all have the same length, and the given procedure
|
|
must accept one argument for each list:
|
|
|
|
@interaction[
|
|
(map (lambda (s n) (substring s 0 n))
|
|
(list "peanuts" "popcorn" "crackerjack")
|
|
(list 6 3 7))
|
|
]
|
|
|
|
The @scheme[foldl] procedure generalizes some iteration procedures. It
|
|
uses the per-element procedure to both process an element and combine
|
|
it with the ``current'' value, so the per-element procedure takes an
|
|
extra first argument. Also, a starting ``current'' value must be
|
|
provided before the lists:
|
|
|
|
@interaction[
|
|
(foldl (lambda (v elem)
|
|
(+ v (* elem elem)))
|
|
0
|
|
'(1 2 3))
|
|
]
|
|
|
|
Despite its generality, @scheme[foldl] is not as popular as the other
|
|
procedures. One reason is that @scheme[map], @scheme[ormap],
|
|
@scheme[andmap], and @scheme[filter] cover the most common kinds of
|
|
list loops.
|
|
|
|
@;------------------------------------------------------------------------
|
|
@section{Iterative Folds and Comprehensions: @scheme[fold-for] and @scheme[list-for]}
|
|
|
|
Besides iteration procedures like @scheme[foldl], Scheme provides a
|
|
syntactic form for iteration that more closely resembles the syntax of
|
|
other languages. The @scheme[foldl] example above can be written with
|
|
the @scheme[fold-for] syntax as follows:
|
|
|
|
@interaction[
|
|
(fold-for ([sum 0])
|
|
([elem (list 1 2 3)])
|
|
(+ sum (* elem elem)))
|
|
]
|
|
|
|
Compare to analogous Java code, where @scheme[(list 1 2 3)] is
|
|
replaced by a collection @scheme[lst]:
|
|
|
|
@verbatim[
|
|
#<<EOS
|
|
int sum = 0;
|
|
for (Object elem : lst) {
|
|
sum = sum + elem * elem;
|
|
}
|
|
return sum;
|
|
EOS
|
|
]
|
|
|
|
The only significant difference is that the updating of @scheme[sum]
|
|
and the return of @scheme[sum]'s value are implicit. Those implicit
|
|
actions are why the form is called @scheme[fold-for] instead of just
|
|
@scheme[for].
|
|
|
|
Along similar lines, the @scheme[list-for] form iterates through a list
|
|
and implicitly accumulates each result into a list:
|
|
|
|
@interaction[
|
|
(list-for ([i (list "peanuts" "popcorn" "crackerjack")])
|
|
(string-append i "!"))
|
|
]
|
|
|
|
The @scheme[list-for] form is a @defterm{list compherension} form, as
|
|
in Haskell, Ruby, Python, and other languages. One advantage over
|
|
@scheme[map] is that it can iterate over more things than just lists.
|
|
For example, @scheme[list-for] can iterate over a range of numbers:
|
|
|
|
@interaction[
|
|
(list-for ([i (range 0 10)])
|
|
i)
|
|
]
|
|
|
|
The @scheme[list-for] form can even iterate over a list and a range of
|
|
numbers in parallel:
|
|
|
|
@interaction[
|
|
(list-for ([s (list "a" "b" "c")]
|
|
[n (range 0 3)])
|
|
(if (= n 2)
|
|
"oops!"
|
|
s))
|
|
]
|
|
|
|
Note that the binding syntax of @scheme[fold-for] and
|
|
@scheme[list-for] is similar to that of @scheme[let] (as introduced in
|
|
@secref["local-binding-intro"]). In the same way that @scheme[let*]
|
|
supports nested bindings, @scheme[list-for*] supports nested
|
|
iterations:
|
|
|
|
@interaction[
|
|
(list-for* ([s (list "a" "b" "c")]
|
|
[n (list "x" "y" "z")])
|
|
(string-append s n))
|
|
]
|
|
|
|
Unlike the @scheme[list-for], the nested iteration of
|
|
@scheme[list-for*] covers patterns with lists not as easily expressed
|
|
with @scheme[map]. When procedures like @scheme[map] suffice, however,
|
|
Scheme programmers tend to use them, partly because the syntax is
|
|
simpler (just a procedure call).
|
|
|
|
We have ignored several other variants of the interation
|
|
form---including plain @scheme[for], which is used when the iteration
|
|
body is to be run only for its effect. For more complete information,
|
|
see @secref["iterations+comprehensions"].
|
|
|
|
@;------------------------------------------------------------------------
|
|
@section{List Iteration from Scratch}
|
|
|
|
Although @scheme[map] and @scheme[list-for] are predefined, they are
|
|
not primitive in any interesting sense. You can write equivalent
|
|
iterations using a handful of list primitives.
|
|
|
|
Since a Scheme list is a linked list, the two core operations on a
|
|
non-empty list are
|
|
|
|
@itemize{
|
|
|
|
@item{@scheme[first]: get the first thing in the list; and}
|
|
|
|
@item{@scheme[rest]: get the rest of the list.}
|
|
|
|
}
|
|
|
|
@examples[
|
|
(first (list 1 2 3))
|
|
(rest (list 1 2 3))
|
|
]
|
|
|
|
To create a new node for a linked list---that is, to add to the front
|
|
of the list---use the @scheme[cons] procedure, which is short for
|
|
``construct.'' To get an empty list to start with, use the
|
|
@scheme[empty] constant:
|
|
|
|
@interaction[
|
|
empty
|
|
(cons "head" empty)
|
|
(cons "dead" (cons "head" empty))
|
|
]
|
|
|
|
To process a list, you need to be able to distinguish empty lists from
|
|
non-empty lists, because @scheme[first] and @scheme[rest] work only on
|
|
non-empty lists. The @scheme[empty?] procedure detects empty lists,
|
|
and @scheme[cons?] detects non-empty lists:
|
|
|
|
@interaction[
|
|
(empty? empty)
|
|
(empty? (cons "head" empty))
|
|
(cons? empty)
|
|
(cons? (cons "head" empty))
|
|
]
|
|
|
|
With these pieces, you can write your own versions of the
|
|
@scheme[length] procedure, @scheme[map] procedure, and more.
|
|
|
|
@defexamples[
|
|
(define (my-length lst)
|
|
(cond
|
|
[(empty? lst) 0]
|
|
[else (+ 1 (my-length (rest lst)))]))
|
|
(my-length empty)
|
|
(my-length (list "a" "b" "c"))
|
|
]
|
|
@def+int[
|
|
(define (my-map f lst)
|
|
(cond
|
|
[(empty? lst) empty]
|
|
[else (cons (f (first lst))
|
|
(my-map f (rest lst)))]))
|
|
(my-map string-upcase (list "ready" "set" "go"))
|
|
]
|
|
|
|
If the derivation of the above definitions is mysterious to you,
|
|
consider reading @|HtDP|. But if you are merely suspicious of the use
|
|
of recursive calls instead of a looping construct, then read on.
|
|
|
|
Both the @scheme[my-length] and @scheme[my-map] procedures run in
|
|
@math{O(n)} time for a list of length @math{n}. This is easy to see by
|
|
imagining how @scheme[(my-length (list "a" "b" "c"))] must evaluate:
|
|
|
|
@schemeblock[
|
|
(my-length (list "a" "b" "c"))
|
|
#,step (+ 1 (my-length (list "b" "c")))
|
|
#,step (+ 1 (+ 1 (my-length (list "c"))))
|
|
#,step (+ 1 (+ 1 (+ 1 (my-length (list)))))
|
|
#,step (+ 1 (+ 1 (+ 1 0)))
|
|
#,step (+ 1 (+ 1 1))
|
|
#,step (+ 1 2)
|
|
#,step 3
|
|
]
|
|
|
|
For a list with @math{n} elements, evalution will stack up @math{n}
|
|
@scheme[(+ 1 ...)] additions, and then finally add them up when the
|
|
list is exhausted.
|
|
|
|
You can avoid piling up additions by adding along the way. To
|
|
accumulate a length this way, we need a procedure that takes both a
|
|
list and the length of the list seem so far; the code below uses a
|
|
local procedure @scheme[iter] that accumulates the length in an
|
|
argument @scheme[len]:
|
|
|
|
@schemeblock[
|
|
(define (my-length lst)
|
|
(code:comment #, @elem{local procedure @scheme[iter]:})
|
|
(define (iter lst len)
|
|
(cond
|
|
[(empty? lst) len]
|
|
[else (iter (rest lst) (+ len 1))]))
|
|
(code:comment #, @elem{body of @scheme[my-length] calls @scheme[iter]:})
|
|
(iter lst 0))
|
|
]
|
|
|
|
Now evaluation looks like this:
|
|
|
|
@schemeblock[
|
|
(my-length (list "a" "b" "c"))
|
|
#,step (iter (list "a" "b" "c") 0)
|
|
#,step (iter (list "b" "c") 1)
|
|
#,step (iter (list "c") 2)
|
|
#,step (iter (list) 3)
|
|
3
|
|
]
|
|
|
|
The revised @scheme[my-length] runs in constant space, just as the
|
|
evaluation steps above suggest. That is, when the result of a
|
|
procedure call, like @scheme[(iter (list "b" "c") 1)], is exactly the
|
|
result of some other procedure call, like @scheme[(iter (list "c")
|
|
2)], then the first one doesn't have to wait around for the second
|
|
one, because that takes up space for no good reason.
|
|
|
|
This evaluation behavior is sometimes called @idefterm{tail-call
|
|
optimization}, but it's not merely an ``optimization'' in Scheme; it's
|
|
a guarantee about the way the code will run.
|
|
|
|
In the case of @scheme[my-map], @math{O(n)} space compelxity is
|
|
reasonable, since it has to generate a result of size
|
|
@math{O(n)}. Nevertheless, you can reduce the constant factor by
|
|
accumulating the result list. The only catch is that the accumulated
|
|
list will be backwards, so you'll have to reverse it at the very end:
|
|
|
|
@schemeblock[
|
|
(define (my-map f lst)
|
|
(define (iter lst backward-result)
|
|
(cond
|
|
[(empty? lst) (reverse backward-result)]
|
|
[else (iter (rest lst)
|
|
(cons (f (first lst))
|
|
backward-result))]))
|
|
(iter lst empty))
|
|
]
|
|
|
|
It turns out that if you write
|
|
|
|
@schemeblock[
|
|
(define (my-map f lst)
|
|
(list-for ([i lst])
|
|
(f i)))
|
|
]
|
|
|
|
then the @scheme[list-for] form in the procedure both is expanded to
|
|
essentially the same code as the @scheme[iter] local definition and
|
|
use. The difference is merely syntactic convenience.
|
|
|
|
@;------------------------------------------------------------------------
|
|
@section{Recursion versus Iteration}
|
|
|
|
The @scheme[my-length] and @scheme[my-map] examples demonstrate that
|
|
iteration is just a special case of recursion. In many languages, it's
|
|
important to try to fit as many computations as possible into
|
|
iteration form. Otherwise, performance will be bad, and moderately
|
|
large inputs can lead to stack overflow. Similarly, in Scheme, it is
|
|
often important to make sure that tail recursion is used to avoid
|
|
@math{O(n)} space consumption when the computation is easily performed
|
|
in constant space.
|
|
|
|
At the same time, recursion does not lead to particularly bad
|
|
performance in Scheme, and there is no such thing as stack overflow;
|
|
you can run out of memory if a computation involves too much context,
|
|
but exhausting memory typically requires orders of magnitude deeper
|
|
recursion than would trigger a stack overflow in other
|
|
languages. These considerations, combined with the fact that
|
|
tail-recursive programs automatically run the same as a loop, lead
|
|
Scheme programmers to embrace recursive forms rather than avoid them.
|
|
|
|
Suppose, for example, that you want to remove consecutive duplicates
|
|
from a list. While such a procedure can be written as a loop that
|
|
remembers the previous element for each iteration, a Scheme programmer
|
|
would more likely just write the following:
|
|
|
|
@def+int[
|
|
(define (remove-dups l)
|
|
(cond
|
|
[(empty? l) empty]
|
|
[(empty? (rest l)) l]
|
|
[else
|
|
(let ([i (first l)])
|
|
(if (equal? i (first (rest l)))
|
|
(remove-dups (rest l))
|
|
(cons i (remove-dups (rest l)))))]))
|
|
(remove-dups (list "a" "b" "b" "b" "c" "c"))
|
|
]
|
|
|
|
In general, this procedure consumes @math{O(n)} space for an input
|
|
list of length @math{n}, but that's fine, since it produces an
|
|
@math{O(n)} result. If the input list happens to be mostly consecutive
|
|
duplicates, then the resulting list can be much smaller than
|
|
@math{O(n)}---and @scheme[remove-dups] will also use much less than
|
|
@math{O(n)} space! The reason is that when the procedure discards
|
|
duplicates, it returns the result of a @scheme[remove-dups] call
|
|
directly, so the tail-call ``optimization'' kicks in:
|
|
|
|
@schemeblock[
|
|
(remove-dups (list "a" "b" "b" "b" "b" "b"))
|
|
#,step (cons "a" (remove-dups (list "b" "b" "b" "b" "b")))
|
|
#,step (cons "a" (remove-dups (list "b" "b" "b" "b")))
|
|
#,step (cons "a" (remove-dups (list "b" "b" "b")))
|
|
#,step (cons "a" (remove-dups (list "b" "b")))
|
|
#,step (cons "a" (remove-dups (list "b")))
|
|
#,step (cons "a" (list "b"))
|
|
#,step (list "a" "b")
|
|
]
|
|
|
|
Tail-call behavior becomes even more important when dealing with
|
|
non-list data or when using an object-oriented style. In the latter
|
|
case, an object must sometimes dispatch to another object; if the
|
|
other object's result is the complete answer, there's no reason for
|
|
the first object to wait around. We defer futher discussion of this
|
|
point until @secref["datatypes"], after which we'll have more forms of
|
|
data to consider.
|
|
|
|
@;------------------------------------------------------------------------
|
|
@section{Named @scheme[let]}
|
|
|
|
As you start reading Scheme code, you'll discover one more form that
|
|
is commonly used to implement iterations and recursive functions:
|
|
@idefterm{named @scheme[let]}. A named @scheme[let] uses the same
|
|
syntactic keyword as a simple sequence of local bindings, but an
|
|
@nonterm{id} after the @scheme[let] (instead of an immediate
|
|
open parenthesis) triggers a different parsing. In general,
|
|
|
|
@schemeblock[
|
|
#, @BNF-seq[@litchar{(} @litchar{let} @nonterm{proc-id} @litchar{(}
|
|
@kleenestar{@BNF-group[@litchar{[} @nonterm{arg-id} @nonterm{init-expr} @litchar{]}]}
|
|
@litchar{)}
|
|
@kleeneplus{@nonterm{body-expr}} @litchar{)}]
|
|
]
|
|
|
|
is equivalent to the sequence
|
|
|
|
@schemeblock[
|
|
#, @BNF-seq[@litchar{(}@litchar{define} @litchar{(} @nonterm{proc-id} @kleenestar{@nonterm{arg-id}} @litchar{)}
|
|
@kleeneplus{@nonterm{body-expr}} @litchar{)}]
|
|
#, @BNF-seq[@litchar{(}@nonterm{proc-id} @kleenestar{@nonterm{init-expr}}@litchar{)}]
|
|
]
|
|
|
|
except that the @scheme[let] form works in any expression
|
|
context.
|
|
|
|
That is, a named @scheme[let] binds a procedure identifier that is
|
|
visible only in the procedure's body, and it implicitly calls the
|
|
procedure with the values of some initial expressions. A named
|
|
@scheme[let] looks similar to the start of @scheme[fold-for], but the
|
|
recursive calls in the body are explicit, and they are not constrained
|
|
to tail position.
|
|
|
|
As an example, here is @scheme[my-map] once again, using a named let
|
|
to bind the local @scheme[iter] procedure:
|
|
|
|
@schemeblock[
|
|
(define (my-map f lst)
|
|
(let iter ([lst lst]
|
|
[backward-result empty])
|
|
(cond
|
|
[(empty? lst) (reverse backward-result)]
|
|
[else (iter (rest lst)
|
|
(cons (f (first lst))
|
|
backward-result))])))
|
|
]
|
|
|
|
Here's another example, where the local @scheme[dup] procedure is used
|
|
recursively and not merely iteratively, and where the traversal of a
|
|
list stops part-way:
|
|
|
|
@def+int[
|
|
(define (duplicate pos lst)
|
|
(let dup ([i 0]
|
|
[lst lst])
|
|
(cond
|
|
[(= i pos) (cons (first lst) lst)]
|
|
[else (cons (first lst) (dup (+ i 1) (rest lst)))])))
|
|
(duplicate 1 (list "apple" "cheese burger!" "banana"))
|
|
]
|