In Chapter 7 I briefly discussed the extended LOOP macro. As I
mentioned then, LOOP provides what is essentially a
special-purpose language just for writing iteration constructs.
This might seem like a lot of bother--inventing a whole language just
for writing loops. But if you think about the ways loops are used in
programs, it actually makes a fair bit of sense. Any program of any
size at all will contain quite a number of loops. And while they
won't all be the same, they won't all be unique either; patterns will
emerge, particularly if you include the code immediately preceding
and following the loops--patterns of how things are set up for the
loop, patterns in what gets done in the loop proper, and patterns in
what gets done after the loop. The LOOP language captures these
patterns so you can express them directly.
The LOOP macro has a lot of parts--one of the main complaints of
LOOP's detractors is that it's too complex. In this chapter,
I'll tackle LOOP head on, giving you a systematic tour of the
various parts and how they fit together.
You can do the following in a LOOP:
Additionally, LOOP provides syntax for the following:
The basic structure of a LOOP is a set of clauses, each of which
begins with a loop keyword.1 How each clause is parsed by the
LOOP macro depends on the keyword. Some of the main keywords,
which you saw in Chapter 7, are for, collecting,
summing, counting, do, and finally.
Most of the so-called iteration control clauses start with the loop
keyword for, or its synonym as,2
followed by the name of a variable. What follows after the variable
name depends on the type of for clause.
The subclauses of a for clause can iterate over the following:
A single loop can have multiple for clauses with each clause
naming its own variable. When a loop has multiple for clauses,
the loop terminates as soon as any for clause reaches its end
condition. For instance, the following loop:
(loop for item in list for i from 1 to 10 do (something))
will iterate at most ten times but may stop sooner if list
contains fewer than ten items.
Arithmetic iteration clauses control the number of times the loop body
will be executed by stepping a variable over a range of numbers,
executing the body once per step. These clauses consist of from one to
three of the following prepositional phrases after the for
(or as): the from where phrase, the to where phrase, and
the by how much phrase.
The from where phrase specifies the initial value of the clause's
variable. It consists of one of the prepositions from,
downfrom, or upfrom followed by a form, which supplies
the initial value (a number).
The to where phrase specifies a stopping point for the loop and
consists of one of the prepositions to, upto,
below, downto, or above followed by a form,
which supplies the stopping point. With upto and
downto, the loop body will be terminated (without executing
the body again) when the variable passes the stopping point; with
below and above, it stops one iteration earlier.The
by how much phrase consists of the prepositions by and a
form, which must evaluate to a positive number. The variable will be
stepped (up or down, as determined by the other phrases) by this
amount on each iteration or by one if it's omitted.
You must specify at least one of these prepositional phrases. The
defaults are to start at zero, increment the variable by one at each
iteration, and go forever or, more likely, until some other clause
terminates the loop. You can modify any or all of these defaults by
adding the appropriate prepositional phrases. The only wrinkle is
that if you want decremental stepping, there's no default from
where value, so you must specify one with either from or
downfrom. So, the following:
(loop for i upto 10 collect i)
collects the first eleven integers (zero to ten), but the behavior of this:
(loop for i downto -10 collect i) ; wrong
is undefined. Instead, you need to write this:
(loop for i from 0 downto -10 collect i)
Also note that because LOOP is a macro, which runs at compile
time, it has to be able to determine the direction to step the
variable based solely on the prepositions--not the values of the
forms, which may not be known until runtime. So, the following:
(loop for i from 10 to 20 ...)
works fine since the default is incremental stepping. But this:
(loop for i from 20 to 10 ...)
won't know to count down from twenty to ten. Worse yet, it won't give
you an error--it will just not execute the loop since i is
already greater than ten. Instead, you must write this:
(loop for i from 20 downto 10 ...)
or this:
(loop for i downfrom 20 to 10 ...)
Finally, if you just want a loop that repeats a certain number of times, you can replace a clause of the following form:
for i from 1 to number-form
with a repeat clause like this:
repeat number-form
These clauses are identical in effect except the repeat clause
doesn't create an explicit loop variable.
The for clauses for iterating over lists are much simpler than
the arithmetic clauses. They support only two prepositional phrases,
in and on.
A phrase of this form:
for var in list-form
steps var over all the elements of the list produced by evaluating list-form.
(loop for i in (list 10 20 30 40) collect i) ==> (10 20 30 40)
Occasionally this clause is supplemented with a by phrase,
which specifies a function to use to move down the list. The default
is CDR but can be any function that takes a list and returns a
sublist. For instance, you could collect every other element of a
list with a loop like this:
(loop for i in (list 10 20 30 40) by #'cddr collect i) ==> (10 30)
An on prepositional phrase is used to step var over the
cons cells that make up a list.
(loop for x on (list 10 20 30) collect x) ==> ((10 20 30) (20 30) (30))
This phrase too can take a by preposition:
(loop for x on (list 10 20 30 40) by #'cddr collect x) ==> ((10 20 30 40) (30 40))
Looping over the elements of a vector (which includes strings and bit
vectors) is similar to looping over the elements of a list except the
preposition across is used instead of in.3 For instance:
(loop for x across "abcd" collect x) ==> (#\a #\b #\c #\d)
Iterating over a hash table or package is slightly more complicated because hash tables and packages have different sets of values you might want to iterate over--the keys or values in a hash table and the different kinds of symbols in a package. Both kinds of iteration follow the same pattern. The basic pattern looks like this:
(loop for var being the things in hash-or-package ...)
For hash tables, the possible values for things are
hash-keys and hash-values, which cause var to be
bound to successive values of either the keys or the values of the
hash table. The hash-or-package form is evaluated once to
produce a value, which must be a hash table.
To iterate over a package, things can be symbols,
present-symbols, and external-symbols, which cause
var to be bound to each of the symbols accessible in a package,
each of the symbols present in a package (in other words, interned or
imported into that package), or each of the symbols that have been
exported from the package. The hash-or-package form is evaluated
to produce the name of a package, which is looked up as if by
FIND-PACKAGE or a package object. Synonyms are also available
for parts of the for clause. In place of the, you can
use each; you can use of instead of in; and you
can write the things in the singular (for example,
hash-key or symbol).
Finally, since you'll often want both the keys and the values when
iterating over a hash table, the hash table clauses support a
using subclause at the end of the hash table clause.
(loop for k being the hash-keys in h using (hash-value v) ...) (loop for v being the hash-values in h using (hash-key k) ...)
Both of these loops will bind k to each key in the hash table
and v to the corresponding value. Note that the first element
of the using subclause must be in the singular
form.4
If none of the other for clauses supports exactly the form of
variable stepping you need, you can take complete control over
stepping with an equals-then clause. This clause is similar to
the binding clauses in a DO loop but cast in a more Algolish
syntax. The template is as follows:
(loop for var = initial-value-form [ then step-form ] ...)
As usual, var is the name of the variable to be stepped. Its
initial value is obtained by evaluating initial-value-form once
before the first iteration. In each subsequent iteration,
step-form is evaluated, and its value becomes the new value of
var. With no then part to the clause, the
initial-value-form is reevaluated on each iteration to provide
the new value. Note that this is different from a DO binding
clause with no step form.
The step-form can refer to other loop variables, including
variables created by other for clauses later in the loop. For
instance:
(loop repeat 5
for x = 0 then y
for y = 1 then (+ x y)
collect y) ==> (1 2 4 8 16)However, note that each for clause is evaluated separately in
the order it appears. So in the previous loop, on the second
iteration x is set to the value of y before y
changes (in other words, 1). But y is then set to the
sum of its old value (still 1) and the new value of x.
If the order of the for clauses is reversed, the results
change.
(loop repeat 5
for y = 1 then (+ x y)
for x = 0 then y
collect y) ==> (1 1 2 4 8)Often, however, you'll want the step forms for multiple variables to
be evaluated before any of the variables is given its new value
(similar to how DO steps its variables). In that case, you can
join multiple for clauses by replacing all but the first
for with and. You saw this formulation already in the
LOOP version of the Fibonacci computation in Chapter 7. Here's
another variant, based on the two previous examples:
(loop repeat 5
for x = 0 then y
and y = 1 then (+ x y)
collect y) ==> (1 1 2 3 5)While the main variables needed within a loop are usually declared
implicitly in for clauses, sometimes you'll need auxiliary
variables, which you can declare with with clauses.
with var [ = value-form ]
The name var becomes the name of a local variable that will cease
to exist when the loop finishes. If the with clause contains
an = value-form part, the variable will be initialized,
before the first iteration of the loop, to the value of
value-form.
Multiple with clauses can appear in a loop; each clause is
evaluated independently in the order it appears and the value is
assigned before proceeding to the next clause, allowing later
variables to depend on the value of already declared variables.
Mutually independent variables can be declared in one with
clause with an and between each declaration.
One handy feature of LOOP I haven't mentioned yet is the ability
to destructure list values assigned to loop variables. This lets you
take apart the value of lists that would otherwise be assigned to a
loop variable, similar to the way DESTRUCTURING-BIND works but a
bit less elaborate. Basically, you can replace any loop variable in a
for or with clause with a tree of symbols, and the list
value that would have been assigned to the simple variable will
instead be destructured into variables named by the symbols in the
tree. A simple example looks like this:
CL-USER> (loop for (a b) in '((1 2) (3 4) (5 6))
do (format t "a: ~a; b: ~a~%" a b))
a: 1; b: 2
a: 3; b: 4
a: 5; b: 6
NILThe tree can also include dotted lists, in which case the name after
the dot acts like a &rest parameter, being bound to a list
containing any remaining elements of the list. This is particularly
handy with for/on loops since the value is always a
list. For instance, this LOOP (which I used in Chapter 18 to
emit a comma-delimited list):
(loop for cons on list
do (format t "~a" (car cons))
when (cdr cons) do (format t ", "))could also be written like this:
(loop for (item . rest) on list
do (format t "~a" item)
when rest do (format t ", "))If you want to ignore a value in the destructured list, you can use
NIL in place of a variable name.
(loop for (a nil) in '((1 2) (3 4) (5 6)) collect a) ==> (1 3 5)
If the destructuring list contains more variables than there are
values in the list, the extra variables are set to NIL, making
all the variables essentially like &optional parameters. There
isn't, however, any equivalent to &key parameters.
The value accumulation clauses are perhaps the most powerful part of
LOOP. While the iteration control clauses provide a concise
syntax for expressing the basic mechanics of looping, they aren't
dramatically different from the equivalent mechanisms provided by
DO, DOLIST, and DOTIMES.
The value accumulation clauses, on the other hand, provide a concise notation for a handful of common loop idioms having to do with accumulating values while looping. Each accumulation clause starts with a verb and follows this pattern:
verb form [ into var ]
Each time through the loop, an accumulation clause evaluates form
and saves the value in a manner determined by the verb. With an
into subclause, the value is saved into the variable named by
var. The variable is local to the loop, as if it'd been declared
in a with clause. With no into subclause, the
accumulation clause instead accumulates a default value for the whole
loop expression.
The possible verbs are collect, append, nconc,
count, sum, maximize, and minimize. Also
available as synonyms are the present participle forms:
collecting, appending, nconcing,
counting, summing, maximizing, and
minimizing.
A collect clause builds up a list containing all the values of
form in the order they're seen. This is a particularly useful
construct because the code you'd have to write to collect a list in
order as efficiently as LOOP does is more painful than you'd
normally write by hand.5 Related to collect are the verbs append and
nconc. These verbs also accumulate values into a list, but they
join the values, which must be lists, into a single list as if by the
functions APPEND or NCONC. 6
The remaining accumulation clauses are used to accumulate numeric
values. The verb count counts the number of times form is
true, sum collects a running total of the values of form,
maximize collects the largest value seen for form, and
minimize collects the smallest. For instance, suppose you
define a variable *random* that contains a list of random
numbers.
(defparameter *random* (loop repeat 100 collect (random 10000)))
Then the following loop will return a list containing various summary information about the numbers:
(loop for i in *random* counting (evenp i) into evens counting (oddp i) into odds summing i into total maximizing i into max minimizing i into min finally (return (list min max total evens odds)))
As useful as the value accumulation constructs are, LOOP wouldn't
be a very good general-purpose iteration facility if there wasn't a
way to execute arbitrary Lisp code in the loop body.
The simplest way to execute arbitrary code within a loop body is with
a do clause. Compared to the clauses I've described so far,
with their prepositions and subclauses, do is a model of
Yodaesque simplicity.7 A do clause
consists of the word do (or doing) followed by one or
more Lisp forms that are all evaluated when the do clause is.
The do clause ends at the closing parenthesis of the loop or
the next loop keyword.
For instance, to print the numbers from one to ten, you could write this:
(loop for i from 1 to 10 do (print i))
Another, more dramatic, form of immediate execution is a return
clause. This clause consists of the word return followed by a
single Lisp form, which is evaluated, with the resulting value
immediately returned as the value of the loop.
You can also break out of a loop in a do clause using any of
Lisp's normal control flow operators, such as RETURN and
RETURN-FROM. Note that a return clause always returns
from the immediately enclosing LOOP expression, while a
RETURN or RETURN-FROM in a do clause can return from
any enclosing expression. For instance, compare the following:
(block outer (loop for i from 0 return 100) ; 100 returned from LOOP (print "This will print") 200) ==> 200
to this:
(block outer (loop for i from 0 do (return-from outer 100)) ; 100 returned from BLOCK (print "This won't print") 200) ==> 100
The do and return clauses are collectively called the
unconditional execution clauses.
Because a do clause can contain arbitrary Lisp forms, you can
use any Lisp expressions you want, including control constructs such
as IF and WHEN. So, the following is one way to write a
loop that prints only the even numbers between one and ten:
(loop for i from 1 to 10 do (when (evenp i) (print i)))
However, sometimes you'll want conditional control at the level of
loop clauses. For instance, suppose you wanted to sum only the even
numbers between one and ten using a summing clause. You
couldn't write such a loop with a do clause because there'd be
no way to "call" the sum i in the middle of a regular Lisp
form. In cases like this, you need to use one of LOOP's own
conditional expressions like this:
(loop for i from 1 to 10 when (evenp i) sum i) ==> 30
LOOP provides three conditional constructs, and they all follow
this basic pattern:
conditional test-form loop-clause
The conditional can be if, when, or unless.
The test-form is any regular Lisp form, and loop-clause can
be a value accumulation clause (count, collect,
and so on), an unconditional execution clause, or another
conditional execution clause. Multiple loop clauses can be attached
to a single conditional by joining them with and.
As an extra bit of syntactic sugar, within the first loop clause,
after the test form, you can use the variable it to refer to
the value returned by the test form. For instance, the following loop
collects the non-NIL values found in some-hash when
looking up the keys in some-list:
(loop for key in some-list when (gethash key some-hash) collect it)
A conditional clause is executed each time through the loop. An
if or when clause executes its loop-clause if
test-form evaluates to true. An unless reverses the test,
executing loop-clause only when test-form is NIL. Unlike
their Common Lisp namesakes, LOOP's if and when
are merely synonyms--there's no difference in their behavior.
All three conditional clauses can also take an else branch,
which is followed by another loop clause or multiple clauses joined by
and. When conditional clauses are nested, the set of clauses
connected to an inner conditional clause can be closed with the word
end. The end is optional when not needed to disambiguate
a nested conditional--the end of a conditional clause will be inferred
from the end of the loop or the start of another clause not joined by
and.
The following rather silly loop demonstrates the various forms of
LOOP conditionals. The update-analysis function will be
called each time through the loop with the latest values of the
various variables accumulated by the clauses within the conditionals.
(loop for i from 1 to 100
if (evenp i)
minimize i into min-even and
maximize i into max-even and
unless (zerop (mod i 4))
sum i into even-not-fours-total
end
and sum i into even-total
else
minimize i into min-odd and
maximize i into max-odd and
when (zerop (mod i 5))
sum i into fives-total
end
and sum i into odd-total
do (update-analysis min-even
max-even
min-odd
max-odd
even-total
odd-total
fives-total
even-not-fours-total))One of the key insights the designers of the LOOP language had
about actual loops "in the wild" is that the loop proper is often
preceded by a bit of code to set things up and then followed by some
more code that does something with the values computed by the loop. A
trivial example, in Perl,8 might look like this:
my $evens_sum = 0;
my $odds_sum = 0;
foreach my $i (@list_of_numbers) {
if ($i % 2) {
$odds_sum += $i;
} else {
$evens_sum += $i;
}
}
if ($evens_sum > $odds_sum) {
print "Sum of evens greater\n";
} else {
print "Sum of odds greater\n";
}The loop proper in this code is the foreach statement. But the
foreach loop doesn't stand on its own: the code in the loop
body refers to variables declared in the two lines before the
loop.9 And
the work the loop does is all for naught without the if
statement after the loop that actually reports the results. In Common
Lisp, of course, the LOOP construct is an expression that
returns a value, so there's even more often a need to do something
after the loop proper, namely, generate the return value.
So, said the LOOP designers, let's give a way to include the
code that's really part of the loop in the loop itself. Thus,
LOOP provides two keywords, initially and finally,
that introduce code to be run outside the loop's main body.
After the initially or finally, these clauses consist
of all the Lisp forms up to the start of the next loop clause or the
end of the loop. All the initially forms are combined into a
single prologue, which runs once, immediately after all the local
loop variables are initialized and before the body of the loop. The
finally forms are similarly combined into a epilogue to be
run after the last iteration of the loop body. Both the prologue and
epilogue code can refer to local loop variables.
The prologue is always run, even if the loop body iterates zero times. The loop can return without running the epilogue if any of the following happens:
return clause executes.RETURN , RETURN-FROM, or another transfer of control
construct is called from within a Lisp form within the body.10always, never, or
thereis clause, as I'll discuss in the next section.Within the epilogue code, RETURN or RETURN-FROM can be used
to explicitly provide a return value for the loop. Such an explicit
return value will take precedence over any value that might otherwise
be provided by an accumulation or termination test clause.
To allow RETURN-FROM to be used to return from a specific loop
(useful when nesting LOOP expressions), you can name a LOOP
with the loop keyword named. If a named clause appears
in a loop, it must be the first clause. For a simple example, assume
lists is a list of lists and you want to find an item that
matches some criteria in one of those nested lists. You could find it
with a pair of nested loops like this:
(loop named outer for list in lists do
(loop for item in list do
(if (what-i-am-looking-for-p item)
(return-from outer item))))While the for and repeat clauses provide the basic
infrastructure for controlling the number of iterations, sometimes
you'll need to break out of a loop early. You've already seen how a
return clause or a RETURN or RETURN-FROM within a
do clause can immediately terminate the loop; but just as
there are common patterns for accumulating values, there are also
common patterns for deciding when it's time to bail on a loop. These
patterns are supported in LOOP by the termination clauses,
while, until, always, never, and
thereis. They all follow the same pattern.
loop-keyword test-form
All five evaluate test-form each time through the iteration and decide, based on the resulting value, whether to terminate the loop. They differ in what happens after they terminate the loop--if they do--and how they decide.
The loop keywords while and until introduce the "mild"
termination clauses. When they decide to terminate the loop, control
passes to the epilogue, skipping the rest of the loop body. The
epilogue can then return a value or do whatever it wants to finish
the loop. A while clause terminates the loop the first time
the test form is false; until, conversely, stops it the first
time the test form is true.
Another form of mild termination is provided by the LOOP-FINISH
macro. This is a regular Lisp form, not a loop clause, so it can be
used anywhere within the Lisp forms of a do clause. It also
causes an immediate jump to the loop epilogue. It can be useful when
the decision to break out of the loop can't be easily condensed into a
single form that can be used with a while or until
clause.
The other three clauses--always, never, and
thereis--terminate the loop with extreme prejudice; they
immediately return from the loop, skipping not only any subsequent
loop clauses but also the epilogue. They also provide a default value
for the loop even when they don't cause the loop to terminate.
However, if the loop is not terminated by one of these termination
tests, the epilogue is run and can return a value other than the
default provided by the termination clauses.
Because these clauses provide their own return values, they can't be
combined with accumulation clauses unless the accumulation clause has
an into subclause. The compiler (or interpreter) should signal
an error at compile time if they are.The always and
never clauses return only boolean values, so they're most
useful when you need to use a loop expression as part of a predicate.
You can use always to check that the test form is true on
every iteration of the loop. Conversely, never tests that the
test form evaluates to NIL on every iteration. If the test form
fails (returning NIL in an always clause or non-NIL
in a never clause), the loop is immediately terminated,
returning NIL. If the loop runs to completion, the default value
of T is provided.
For instance, if you want to test that all the numbers in a list,
numbers, are even, you can write this:
(if (loop for n in numbers always (evenp n))
(print "All numbers even."))Equivalently you could write the following:
(if (loop for n in numbers never (oddp n))
(print "All numbers even."))A thereis clause is used to test whether the test form is
ever true. As soon as the test form returns a non-NIL value,
the loop is terminated, returning that value. If the loop runs to
completion, the thereis clause provides a default return value
of NIL.
(loop for char across "abc123" thereis (digit-char-p char)) ==> 1 (loop for char across "abcdef" thereis (digit-char-p char)) ==> NIL
Now you've seen all the main features of the LOOP facility. You
can combine any of the clauses I've discussed as long as you abide by
the following rules:
named clause, if any, must be the first clause.named clause come all the initially,
with, for, and repeat clauses.finally clauses.The LOOP macro will expand into code that performs the following
actions:
with or for clauses as well as those implicitly created
by accumulation clauses. The initial value forms are evaluated in the
order the clauses appear in the loop.initially clauses--the
prologue--in the order they appear in the loop.finally clauses--the
epilogue--in the order they appear in the loop.While the loop is iterating, the body is executed by first stepping any iteration control variables and then executing any conditional or unconditional execution, accumulation, or termination test clauses in the order they appear in the loop code. If any of the clauses in the loop body terminate the loop, the rest of the body is skipped and the loop returns, possibly after running the epilogue.
And that's pretty much all there is to it.12 You'll use
LOOP fairly often in the code later in this book, so it's worth
having some knowledge of it. Beyond that, it's up to you how much you
use it.
And with that, you're ready to dive into the practical chapters that make up the rest of the book--up first, writing a spam filter.
1The term loop keyword is a
bit unfortunate, as loop keywords aren't keywords in the normal sense
of being symbols in the KEYWORD package. In fact, any symbol,
from any package, with the appropriate name will do; the LOOP
macro cares only about their names. Typically, though, they're
written with no package qualifier and are thus read (and interned as
necessary) in the current package.
2Because one of the
goals of LOOP is to allow loop expressions to be written with a
quasi-English syntax, many of the keywords have synonyms that are
treated the same by LOOP but allow some freedom to express
things in slightly more idiomatic English for different contexts.
3You may
wonder why LOOP can't figure out whether it's looping over a
list or a vector without needing different prepositions. This is
another consequence of LOOP being a macro: the value of the list
or vector won't be known until runtime, but LOOP, as a macro,
has to generate code at compile time. And LOOP's designers
wanted it to generate extremely efficient code. To be able to
generate efficient code for looping across, say, a vector, it needs
to know at compile time that the value will be a vector at
runtime--thus, the different prepositions are needed.
4Don't ask me why LOOP's authors chickened out on the
no-parentheses style for the using subclause.
5The trick is to keep ahold of the tail
of the list and add new cons cells by SETFing the CDR of
the tail. A handwritten equivalent of the code generated by
(loop for i upto 10 collect i) would look like this:
(do ((list nil) (tail nil) (i 0 (1+ i)))
((> i 10) list)
(let ((new (cons i nil)))
(if (null list)
(setf list new)
(setf (cdr tail) new))
(setf tail new)))Of course you'll rarely, if ever, write code like that. You'll use
either LOOP or (if, for some reason, you don't want to use
LOOP) the standard PUSH/NREVERSE idiom for collecting
values.
6Recall that NCONC is
the destructive version of APPEND--it's safe to use an
nconc clause only if the values you're collecting are fresh
lists that don't share any structure with other lists. For instance,
this is safe:
(loop for i upto 3 nconc (list i i)) ==> (0 0 1 1 2 2 3 3)
But this will get you into trouble:
(loop for i on (list 1 2 3) nconc i) ==> undefined
The later will most likely get into an infinite loop as the various parts of the list produced by (list 1 2 3) are destructively modified to point to each other. But even that's not guaranteed--the behavior is simply undefined.
7"No! Try not. Do . . . or do not. There is no try." -- Yoda, The Empire Strikes Back
8I'm not picking on Perl here--this example would look pretty much the same in any language that bases its syntax on C's.
9Perl would let you get away with not declaring those
variables if your program didn't use strict. But you should
always use strict in Perl. The equivalent code in Python,
Java, or C would always require the variables to be declared.
10You
can cause a loop to finish normally, running the epilogue, from Lisp
code executed as part of the loop body with the local macro
LOOP-FINISH.
11Some Common Lisp
implementations will let you get away with mixing body clauses and
for clauses, but that's strictly undefined, and some
implementations will reject such loops.
12The one aspect of
LOOP I haven't touched on at all is the syntax for declaring the
types of loop variables. Of course, I haven't discussed type
declarations outside of LOOP either. I'll cover the general
topic a bit in Chapter 32. For information on how they work with
LOOP, consult your favorite Common Lisp reference.