Common Lisp iteration
Created Monday 23 November 2020
Each of the following definitions of a factorial function demonstrate a way to iterate in Common Lisp, with brief notes. I hope that by demonstrating many different ways that the same thing can be written, you can develop a sense for the character of the constructs afforded by the language, and of the variety of possible styles. Common Lisp is famously syntactically extensible via macros, so keep in mind that my examples are by no means the only ways to iterate.
For further reading on the iteration and control structures of Common Lisp, I heartily recommend:
- Chapter 7 and Chapter 22 of Practical Common Lisp by Peter Siebel.
- A reasonably-priced used copy of ANSI Common Lisp by Paul Graham.
Note: several of the examples return nonsensical results for negative inputs. The addition of (assert (not (minusp n)) or similar is a good idea, but I have omitted it here for clarity.
DOTIMES
(defun factorial-dotimes (n &aux (prod 1))
(dotimes (i n prod)
(setq prod (* prod (1+ i)))))
&auxlambda list keyword names a local variableprod.LETcould also be used for this purpose, but at the cost of more indentation.DOTIMESbindsisuccessively from 0 to 1-n and finally evaluates toprod.
DO
(defun factorial-do (n)
(do ((i 1 (1+ i))
(prod 1 (* prod i)))
((> i n) prod)))
DObindsito 1 and then to (1+ i) in subsequent iterations.prodis bound first to 1 and then to(* prod i)in subsequent iterations.- When the test clause
(> i n)becomes true,prodis returned. Contrast with the test clause offorloops in other languages, which terminate the loop when they become false. - I like the way Paul Graham explains
DOandDO*in ANSI Common Lisp.
LOOP
(defun factorial-loop (n)
(loop
for i from 1 to n
for prod = 1 then (* prod i)
finally (return prod)))
iis bound from 1 toninclusive.prodis bound to 1 and then(* prod i)in subsequent iterations in a manner similar toDO.- In the
finallyclause,prodis returned byRETURNonce iteration is complete. TheBLOCKnamed NIL established byLOOPis the point of return. LOOPsupports a comprehensive iteration and accumulation DSL. Chapter 22 of Practical Common Lisp offers a great introduction.
The preceding example demonstrates the "extended" form of LOOP. There's also "simple" form:
(defun factorial-simple-loop (n &aux (i 0) (prod 1))
(loop
(when (eql i n)
(return prod))
(setq prod (* prod (incf i)))))
Recursion
(defun factorial-recursive (n)
(if (zerop n)
1
(* n (factorial-recursive (1- n)))))
-
FACTORIAL-RECURSIVEcalls itself, but whennexceeds the maximum stack size supported by the implementation, an error is signaled.(defun factorial-tail-recursive (n) (labels ((recur (n prod) (if (zerop n) prod (recur (1- n) (* n prod))))) (recur n 1)))
-
FACTORIAL-TAIL-RECURSIVEdoes not call itself directly. -
Instead, it defines with
LABELSan internal and recursive helper function,recur. -
recur calls itself in tail position and the stack never overflows in implementations that implement tail-call elimination.
(defun factorial-tail-recursive-opt (n &optional (prod 1)) (if (zerop n) prod (factorial-tail-recursive-opt (1- n) (* n prod))))
-
FACTORIAL-TAIL-RECURSIVE-OPTis also tail recursive, but uses the&OPTIONALlambda list keyword to maintainprodacross iterations. This approach has the downside of exposingprodas part of the public interface of the function. Arguably,prodis an implementation detail, best kept internal.
PROG
(defun factorial-prog (n)
(prog ((i 0) (prod 1))
begin
(when (eql i n)
(return prod))
(setq prod (* prod (incf i)))
(go begin)))
- PROG supports both declaring local lexical variables (
iandprod) and naming GO tags (begin). beginnames a label within the implicitTAGBODYenclosed byPROGthat may be jumped to.WHENiisEQLton,RETURNreturnsprod.GOjumps tobegin.
TAGBODY
(defun factorial-tagbody (n &aux (i 0) (prod 1))
(tagbody
begin
(when (eql i n)
(return-from factorial-tagbody prod))
(setq prod (* prod (incf i)))
(go begin)))
TAGBODYis the most general but also the lowest-level and most verbose iteration construct.&auxlambda list keyword names local variablesiandprod, initializing them to 0 and 1, respectively.WHENiisEQLton,RETURN-FROMreturnsprodfrom theBLOCKnamed after the function byDEFUN.GOjumps tobegin.