user3414663
user3414663

Reputation: 583

Defining a macro for iterate

I wanted to define a new clause for the iterate macro. Something similar to Python's range where you have a start, stop, step. Here's a first try:

(defmacro-clause (for var start start stop stop step step)
  (if (minusp step)
      `(for ,var from ,start downto ,stop by (- ,step))
      `(for ,var from ,start to     ,stop by ,step)))

It deals with increasing and decreasing ranges using the to and downto keywords of iterate. (Note that, unlike Python, these include the stop value.)

This works as desired for

(iter (for x start 5 stop 3 step -1)
      (collect x))

;; => (5 4 3)

(iter (for x start 2 stop 5 step 1)
      (collect x))

;; => (3,4,5)

However it fails for anything like

(let ((a 9)
      (b 3)
      (c -1))
  (iter (for x start a stop b step c)
        (collect x)))

Is it a quirk of iterate that it requires explicit numbers in these places? It has no problem with things like

(iter (for x below (+ 3 3) by (+ 1 1))
      (collect x))

Concretely my question is, how can I define a new iterate clause that accepts variables which are bound to numbers in these places?

Upvotes: 3

Views: 482

Answers (1)

ignis volens
ignis volens

Reputation: 9252

The problem is that you are trying to decide things at macro-expansion time which can't be known then, such as the sign of a variable. In particular you can't write a macro which expands into (part of) another macro depending on anything which is only known at run time, or you can, but that necessarily means you have to call the moral equivalent of eval at run-time, and ... don't do that.

Instead you have to make the decision about which way to count at run-time. This means you can't use any of the (for var from ...) or related clauses because there don't seem to be any which are agnostic about direction (why (for i from 1 to -5 by -1) doesn't work is beyond me but ... well).

So whatever clause you end up with needs to expand into a (for var next ...) clause, I think.

Here is an attempt at such. Disclaimer: not tested very much, I don't use iterate, may explode on contact, poisonous to fish.

(defmacro-driver (for v in-range a to b &optional by s)
  (let ((firstp (make-symbol "FIRSTP"))
        (value (make-symbol "VALUE"))
        (limit (make-symbol "LIMIT"))
        (step (make-symbol "STEP")))
    `(progn
       (with ,firstp = t)
       (with ,value = (let ((v ,a))
                        (unless (numberp v)
                          (warn "doomed"))
                        (when (null v)
                          (warn "extremely doomed"))
                        v))
       (with ,limit = (let ((v ,b))
                        (unless (numberp v)
                          (warn "also doomed"))
                        v))
       (with ,step = (let ((v (or ,s (signum (- ,limit ,value)))))
                       (when (not (numberp v))
                         (warn "doomed again"))
                       (when (zerop v)
                         (warn "zero step"))
                       (when (not (= (signum v) (signum (- ,limit ,value))))
                         (warn "likely doomed"))
                       v))
       (,(if generate 'generate 'for)
        ,v
        next (if ,firstp
                 (progn
                   (setf ,firstp nil)
                   ,value)
               (progn
                 (incf ,value ,step)
                 (when (if (> ,step 0)
                           (>= ,value ,limit)
                         (<= ,value ,limit))
                   (terminate))
                 ,value))))))

And now

> (iter (for i in-range 1 to 5 by 2)
    (print i))

1 
3
nil

> (iter (for i in-range 1 to -1)
    (print i))

1
0
nil

> (iter (for i in-range 1 to 5 by -2)
    (when (< i -20)
      (terminate)))
Warning: likely doomed
nil

Obviously some of the checks could be better.

Upvotes: 2

Related Questions