8THE LOOP MACRO

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.

8.1THE LOOP MACRO

Simple Loops

The simplest loop runs forever:

Buffer

almighty/the-loop-macro.lisp

Package

cl-user

(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.

8.2THE LOOP MACRO

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 convenient 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))
8.3THE LOOP MACRO

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")
Returns
(#\H #\E #\L #\L #\O)

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

8.4THE LOOP MACRO

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

Iterate over keys only:

(loop :for key :being :the :hash-keys :of *ht*
      :do (format t "Key: ~A~%" key))
Returns
Key: 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))
Returns
Value: 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))
Returns
Key: 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))
Returns
Key: A  Value: 10
Key: B  Value: 20
Key: foo  Value: 30
 => NIL
8.5THE LOOP MACRO

Numeric Iteration

You can, of course, iterate 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:

Buffer

almighty/loop.lisp

Package

almighty-loop

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

The destructive version of reverse. It may modify its argument, so only use it on freshly built lists like this one.

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)
8.6THE LOOP MACRO

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 flatten 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:

Buffer

almighty/loop.lisp

Package

almighty-loop

(count-if #'oddp '(3 1 4 1 5 9 2 6))
8.7THE LOOP MACRO

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.

8.8THE LOOP MACRO

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))
8.9THE LOOP MACRO

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:

Buffer

almighty/loop.lisp

Package

almighty-loop

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

incf and decf are setf-style macros; they modify the value stored in a place.

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.

8.10THE LOOP MACRO

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))
Returns
("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))
Returns
((#\a 1) (#\b 2) (#\c 3) (#\d 4) (#\e 5))
8.11THE LOOP MACRO

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))
Returns
("Alice is 30 years old" "Bob is 25 years old" "Charlie is 35 years old")

This is particularly useful for association lists:

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

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

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

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

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))
Returns
NAME: 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.

8.12THE LOOP MACRO

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)
Returns
(1 2 4 8 16 32 64 128)
8.13THE LOOP MACRO

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.~%"))

Returns
Starting...
Processing A
Processing B
Processing C
Done.
=> NIL
8.14THE LOOP MACRO

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))))
Returns
(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.

8.15THE LOOP MACRO

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)))
Returns
(: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.

8.16THE LOOP MACRO

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.

8.17THE LOOP MACRO

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.