Generating sequences in Common Lisp

Python provides a useful function for enumerating a sequence of numbers in the form of the range function. If you are looking for range function in Common Lisp you would find lots of various implementations.

I think this is a good exercise to practice in any new language you are learning, so here I’m going to post my own implementation of range in Common Lisp.

First, we are going to implement iota sequences which we will later use as a helper for implementing range. The following implementation of IOTA uses the DO iteration macro.

(defun iota (n &key (start 0) (step 1))
  "iota using DO iteration macro"
  (when (minusp n)
    (error "Invalid number of items specified"))
  (do ((i 0 (1+ i))
       (item start (+ item step))
       (result nil (push item result)))
      ((= i n) (nreverse result))))

We can now use IOTA to generate a sequence of numbers, e.g.

CL-USER> (iota 10)
(0 1 2 3 4 5 6 7 8 9)
CL-USER> (iota 10 :start 0 :step -1)
(0 -1 -2 -3 -4 -5 -6 -7 -8 -9)

With slight modifications to above function we can turn it into a macro.

(defmacro with-iota ((var n &key (start 0) (step 1)) &body body)
  "Generates a sequence of numbers and bind VAR to each item in the sequence"
  (let ((i-var (gensym))
        (num-items (gensym)))
    `(let ((,num-items ,n))
       (when (minusp ,num-items)
         (error "Invalid number of items specified"))
       (do ((,i-var 0 (1+ ,i-var))
            (,var ,start (+ ,var ,step)))
           ((>= ,i-var ,num-items))
         ,@body))))

The reason we use num-items above is because we want to evaluate the form represented by n only once. We could have used alexandria:once-only macro for that as well, but I wanted to keep things self-contained here. Example usage of our WITH-IOTA macro looks like this.

CL-USER> (with-iota (i 5)
           (format t "Got number ~a~%" i))
Got number 0
Got number 1
Got number 2
Got number 3
Got number 4
NIL

Another possible implementation of IOTA would be to make it generate new items from the sequence when needed. The following implementation behaves like a generator. Basically what the function does is to create a closure over the number of items we need to produce.

(defun iota-generator (n &key (start 0) (step 1))
  (let ((i 0))
    (lambda ()
      (unless (>= i n)
	(prog1 start (incf start step) (incf i))))))

Example usage of our IOTA-GENERATOR. The generator will return NIL when the sequence has been exhausted.

CL-USER> (defparameter *iota-gen* (iota-generator 3 :start 42 :step -42))
*IOTA-GEN*
CL-USER> (funcall *iota-gen*)
42
CL-USER> (funcall *iota-gen*)
0
CL-USER> (funcall *iota-gen*)
-42
CL-USER> (funcall *iota-gen*)
NIL

Calling (funcall *iota-gen*) from the REPL is fine, but what we would really want to do is to wrap our generator in a macro like this for example.

(defmacro with-iota ((i n &key (start 0) (step 1)) &body body)
  (let ((generator-var (gensym)))
    `(let ((,generator-var (iota-generator ,n :start ,start :step ,step)))
       (loop for ,i = (funcall ,generator-var) then (funcall ,generator-var)
	     while ,i do
	       ,@body))))

Having sorted out IOTA we can now go ahead and implement the RANGE function. Here’s one possible implementation of RANGE.

(defun range (start stop &key (step 1))
  "range implemented using IOTA"
  (when (and (> start stop) (plusp step))
    (error (format nil "Invalid positive step: start=~a, stop=~a, step=~a" start stop step)))
  (when (and (< start stop) (minusp step))
    (error (format nil "Invalid negative step: start=~a, stop=~a, step=~a" start stop step)))
  (when (zerop step)
    (error (format nil "Invalid zero step: start=~a, stop=~a, step=~a" start stop step)))
  (let ((n (abs (ceiling (- start stop) (abs step)))))
    (iota n :start start :step step)))

Most of the code above is just validations to make sure that we have valid inputs, while the actual generation of the sequence is just a couple of lines. Here’s how we can use our RANGE function.

CL-USER> (range 1 10)
(1 2 3 4 5 6 7 8 9)
CL-USER> (range 10 1 :step -1)
(10 9 8 7 6 5 4 3 2)

We could have implemented RANGE without using IOTA, but re-using what we already have is always a good thing. If we wanted to implemented RANGE without having to rely on IOTA we could have implemented a recursive solution as well, e.g.

(defun range (start stop &key (step 1))
  "Recursive RANGE function"
  (when (and (> start stop) (plusp step))
    (error (format nil "Invalid positive step: start=~a, stop=~a, step=~a" start stop step)))
  (when (and (< start stop) (minusp step))
    (error (format nil "Invalid negative step: start=~a, stop=~a, step=~a" start stop step)))
  (when (zerop step)
    (error (format nil "Invalid zero step: start=~a, stop=~a, step=~a" start stop step)))
  (labels ((recur (i acc)
	     (cond
	       ((and (minusp step) (<= i stop)) (nreverse acc))
	       ((and (plusp step) (>= i stop)) (nreverse acc))
	       (t (recur (+ i step) (push i acc))))))
    (recur start nil)))

Note, that instead of re-inventing the wheel over and over again you would probably want to use the functions already provided by libraries such as Alexandria and series. The code provided here is merely used for practicing purposes.

Written on January 31, 2020