8

THE LOOP MACRO

In the chapter on Lisp Fundamentals, we briefly touched on the loop macro. However, with a deeper knowledge of the loop macro, you can replace the use of several built-in functions with just loop. It also has the advantage of being easier to understand for those that are familiar with other languages (although that led it to being somewhat controversial among lispers when it was introduced).

Given the amount of utility you can get out of a good understanding of loop, we'll take a closer look at it, comparing to other built-in functions along the way.

Simple Loops

The simplest loop runs forever:

(loop (print "forever")
      (return))

This is the "simple" form of loop. It takes a body with no clauses and repeats it. You'll rarely use this form–it's only useful when combined with return or return-from to break out manually. The "extended" form, which uses clauses like :for and :collect, is what the rest of this chapter covers.

Iterating Over Lists

The most basic pattern iterates over the elements of a list:

(loop :for item :in '(one two three)
      :do (print item))

Compare this with dolist:

(dolist (item '(one two three))
  (print item))

They do the same thing. dolist is shorter for this case. When you need to do many operations, however, then you'll find loop is more efficient and easier to understand.

Where :in binds the variable to each element of the list, :on binds it to each successive sublist:

(loop :for sublist :on '(a b c d)
      :do (print sublist))

Iterating Over Vectors and Strings

:in works on lists. For vectors (including strings), use :across:

(loop :for char :across "hello"
      :collect (char-upcase char))
(loop :for elem :across #(10 20 30 40)
      :sum elem)

Compare with map:

(map 'list #'char-upcase "hello")
Result(#####)

map is more concise here, but loop lets you mix in conditions, multiple accumulations, and other clauses that map can't express.

Iterating over Hash-Tables

There are special keywords for working with hash-tables as well: :being :the :hash-keys :of, :being :the :hash-values :of, :using (hash-value value), and :using (hash-key key).

(defparameter *ht* (make-hash-table))
(setf (gethash 'a *ht*) 10)
(setf (gethash 'b *ht*) 20)
(setf (gethash "foo" *ht*) 30)   ; string key
Result30

Iterate over keys only:

(loop :for key :being :the :hash-keys :of *ht*
      :do (format t "Key: ~A~%" key))
ResultKey: A
Key: B
Key: foo
 => NIL

Iterate over values only:

(loop :for value :being :the :hash-values :of *ht*
      :do (format t "Value: ~A~%" value))
ResultValue: 10
Value: 20
Value: 30
 => NIL

More commonly, you want both keys and values during your loops:

(loop :for key :being :the :hash-keys :of *ht*
      :using (hash-value value)
      :do (format t "Key: ~A  Value: ~A~%" key value))
ResultKey: A  Value: 10
Key: B  Value: 20
Key: foo  Value: 30
 => NIL

Of course, you can start with the values instead:

(loop :for value :being :the :hash-values :of *ht*
      :using (hash-key key)
      :do (format t "Key: ~A  Value: ~A~%" key value))
ResultKey: A  Value: 10
Key: B  Value: 20
Key: foo  Value: 30
 => NIL

Numeric Iteration

You can, of course, iterated over number ranges. You can run the loop body a fixed number of times with :repeat:

(loop :repeat 3
      :do (print "hello"))

If you need an "index" variable, then use :from ... :to ...

(loop :for i :from 0 :to 4
      :collect i)

:to is inclusive. If you want exclusive, use :below:

(loop :for i :from 0 :below 4
      :collect i)

Compare with dotimes:

(let ((result nil))
  (dotimes (i 4 (nreverse result))
    (push i result)))

dotimes gets the job done, but loop with :collect saves you from manually pushing and reversing.

You can count down:

(loop :for i :from 10 :downto 1
      :do (format t "~a... " i))

:above is the exclusive version of :downto:

(loop :for i :from 10 :above 0
      :collect i)

And you can step by any amount:

(loop :for i :from 0 :to 20 :by 5
      :collect i)

Accumulation Clauses

:collect is the most common accumulation clause, but there are several others. It builds a list from the values:

(loop :for n :in '(1 2 3 4 5)
      :collect (* 2 (sqrt n)))

If you need to flattens lists of lists, use :append:

(loop :for sublist :in '((1 2) (3 4) (5 6))
      :append sublist)

Compare with reduce:

(reduce #'append '((1 2) (3 4) (5 6)))

:nconc does the same thing destructively (faster, but mutates the sublists). Use :append unless you're sure the sublists are freshly created and won't be needed again.

You can accumulate via summing with :sum or counting with :count:

(loop :for n :in '(3 1 4 1 5 9 2 6)
      :sum n)

(loop :for n :in '(3 1 4 1 5 9 2 6)
      :count (oddp n))

Compare :sum with reduce:

(reduce #'+ '(3 1 4 1 5 9 2 6))

You can get the maximum or minimum values in some collection with :maximize and :minimize:

(loop :for n :in '(3 1 4 1 5 9 2 6)
      :maximize n)

(loop :for n :in '(3 1 4 1 5 9 2 6)
      :minimize n)

Compare :count with count-if:

(count-if #'oddp '(3 1 4 1 5 9 2 6))

Conditional Clauses

You can filter data with when and unless clauses:

(loop :for n :in '(1 2 3 4 5 6 7 8 9 10)
      :when (evenp n)
        :collect n)

Compare with remove-if-not:

(remove-if-not #'evenp '(1 2 3 4 5 6 7 8 9 10))

:unless is the opposite of :when:

(loop :for n :in '(1 2 3 4 5 6 7 8 9 10)
      :unless (evenp n)
        :collect n)

For more complex branching, use :if, :else, :end, and :finally:

(loop :for n :in '(1 2 3 4 5 6 7 8 9 10)
      :if (evenp n)
        :collect n :into evens
      :else
        :collect n :into odds
      :end
      :finally (return (list :evens evens :odds odds)))

The :end marks where the :if / :else block ends. :into collects into a named variable instead of the loop's default return value, and :finally runs code after the loop finishes.

Termination

:while continues looping as long as the condition is true. :until stops as soon as the condition becomes true:

(loop :for n :in '(2 4 6 7 8 10)
      :while (evenp n)
      :collect n)

The loop stopped at 7 because it's odd. Note that 8 and 10 are not visited at all--:while terminates the loop immediately.

This is useful for reading data from a file:

(with-open-file (stream #P"some-file.txt" :direction :input)
  (loop :for line = (read-line stream nil nil)
        :while line
        :collect line))

Here :for line \= assigns the result of (read-line ...) to line each iteration, and :while line stops when read-line returns nil (end of file).

You can exit a loop early with return:

(loop :for n :in '(2 4 6 7 8 10)
      :when (oddp n)
        :do (return n))

:thereis, :always, and :never are shorthand for common patterns:

(loop :for n :in '(2 4 6 8 10)
      :always (evenp n))

(loop :for n :in '(2 4 6 7 8 10)
      :always (evenp n))

(loop :for n :in '(2 4 6 7 8 10)
      :thereis (oddp n))

(loop :for n :in '(2 4 6 8 10)
      :never (oddp n))

Compare with the standard functions:

(every #'evenp '(2 4 6 8 10))
(some #'oddp '(2 4 6 7 8 10))
(notany #'oddp '(2 4 6 8 10))

Manually Updated Variable Bindings

:with creates a local variable that persists across iterations but is not a loop variable. It won't step or update automatically. Here, the total is manually incremented with incf:

(loop :with total = 0
      :for n :in '(1 2 3 4 5)
      :do (incf total n)
      :finally (return total))

Of course, :sum would be simpler here. :with is more useful when you need to maintain state that doesn't fit neatly into an accumulation clause:

(loop :with prev = nil
      :for n :in '(1 1 2 3 3 3 4 5 5)
      :unless (eql n prev)
        :collect n
      :do (setf prev n))

This removes consecutive duplicates–something that would require a let and manual bookkeeping with dolist.

Parallel Iterating

You can iterate over multiple things in parallel. The loop ends when the shortest sequence runs out:

(loop :for name :in '("Alice" "Bob" "Charlie")
      :for i :from 1
      :collect (format nil "~a. ~a" i name))
Result("1. Alice" "2. Bob" "3. Charlie")

Compare with mapcar:

(let ((i 0))
  (mapcar (lambda (name)
            (incf i)
            (format nil "~a. ~a" i name))
          '("Alice" "Bob" "Charlie")))

The loop version is cleaner because it has a built-in counter. No need for an external variable.

You can also iterate over different types of sequences in parallel:

(loop :for letter :across "abcde"
      :for number :in '(1 2 3 4 5)
      :collect (list letter number))
Result((#1) (#2) (#3) (#4) (#5))

Destructuring

loop can destructure list elements, pulling them apart as it iterates:

(loop :for (name age) :in '(("Alice" 30) ("Bob" 25) ("Charlie" 35))
      :collect (format nil "~a is ~a years old" name age))
Result("Alice is 30 years old" "Bob is 25 years old" "Charlie is 35 years old")

This is particularly useful for association lists:

(defparameter *people*
  '((:micah male 40 japan)
    (:takae female 35 japan)
    (:mom female 71 america)))

(loop :for (name gender age country) :in *people*
      :when (eql country 'japan)
        :collect name)
Result(:MICAH :TAKAE)

Without loop destructuring, you'd need first, second, third, etc.:

(mapcar #'first
        (remove-if-not (lambda (row) (eql (fourth row) 'japan))
                        *people*))

The loop version reads more naturally, especially as the structure gets more complex.

If you want to step through a plist you can combine :on with :by:

(loop :for (key value) :on '(:name "Micah" :age 40 :lang "Lisp")
        :by #'cddr
      :do (format t "~&~a: ~a" key value))
ResultNAME: Micah
AGE: 40
LANG: Lisp
=> NIL

:by specifies the stepping function. The default is #'cdr (move one element forward). Using #'cddr moves two elements forward, which is exactly what you need for key-value pairs.

The Step Binding

Sometimes you need to compute a value each iteration rather than pull it from a sequence:

(loop :for line = (read-line *standard-input* nil nil)
      :while line
      :collect line)

This assigns the result of the expression to line each time through. Without :then, the same expression is evaluated every iteration.

With :then, you can specify different expressions for the first iteration and subsequent ones:

(loop :for x = 1 :then (* x 2)
      :repeat 8
      :collect x)
Result(1 2 4 8 16 32 64 128)

Running Code Before The First Iteration Or After The Last Iteration

:initially runs code before the first iteration. :finally runs code after the last iteration:

(loop :for n :in '(1 2 3 4 5)
      :sum n :into total
      :finally (return (/ total 5)))

This computes an average. :finally has access to variables created by :into, which makes it useful for post-processing accumulated results.

(loop :initially (format t "~&Starting...~%")
      :for item :in '(a b c)
      :do (format t "~&Processing ~a~%" item)
      :finally (format t "~&Done.~%"))

ResultStarting...
Processing A
Processing B
Processing C
Done.
=> NIL

Returning From Named Loops

You can name a loop with :named and exit it with return-from. This is useful for nested loops:

(loop :named outer
      :for x :in '(1 2 3)
      :do (loop :for y :in '(10 20 30)
                :when (and (= x 2) (= y 20))
                  :do (return-from outer (list x y))))
Result(2 20)

Without :named, a return from the inner loop would only exit the inner loop. return-from lets you break out of any enclosing named block.

Combining Everything

The real power of loop shows when you combine clauses. Here's a single pass over a list that does several things at once:

(loop :for n :in '(3 1 4 1 5 9 2 6 5 3 5)
      :count (oddp n) :into odd-count
      :sum n :into total
      :maximize n :into biggest
      :when (evenp n)
        :collect n :into evens
      :finally (return (list :odd-count odd-count
                             :total total
                             :biggest biggest
                             :evens evens)))
Result(:ODD-COUNT 8 :TOTAL 44 :BIGGEST 9 :EVENS (4 2 6))

Doing this without loop would require a let with four variables and a dolist with manual bookkeeping for each accumulation. The loop version declares what you want and lets the macro handle the mechanics.

When to Use Something Else

loop is not always the right tool.

For a simple transformation of each element, mapcar is more concise:

(mapcar #'1+ '(1 2 3))
(loop :for n :in '(1 2 3) :collect (1+ n))

For a simple predicate test, every, some, notany are clearer:

(every #'evenp numbers)
(loop :for n :in numbers :always (evenp n))

For reducing to a single value with a known function, reduce says it directly:

(reduce #'+ numbers)
(loop :for n :in numbers :sum n)

For a simple side effect on each element, dolist is fine:

(dolist (item items) (print item))
(loop :for item :in items :do (print item))

Use loop when the iteration is complex enough that you'd otherwise need to combine multiple constructs, maintain manual state, or make multiple passes over the data. If a simpler construct expresses your intent clearly, prefer it.

A Note on Style: Keywords vs Symbols

You'll see loop written two ways:

;; With keywords (this book's style):
(loop :for i :from 0 :below 5 :collect i)

;; With plain symbols:
(loop for i from 0 below 5 collect i)

Both are valid. The keyword style (with colons) makes the loop clauses visually distinct from variables and expressions. This helps make inner variables (like i above) distinct and easy to find. Prefer the keyword style.