Should I avoid resetting "by hand" an auto-incremented loop variable?
Asked Answered
R

3

6

I have a loop containing several variables; one of them is incremented at each step. Sometimes however, this variable may be reset to 0. Thus I can write:

(loop
    with z = 0
    ...
    do (progn
           (setq (z (1+ z)))
           ...
           (if ...
               (setq z 0))))

but it looks I can also write:

(loop
    for z from 1
    ...
    do (progn
           ...
           (if ...
               (setq z 0))))

The second version is shorter (by one line); but I am not sure it is really clean; is it fully compliant with Lisp standard?

Ruysdael answered 3/1, 2020 at 14:41 Comment(0)
I
6

I think you should avoid doing so, for two reasons.

Firstly as far as I can see the loop specification makes no clear statement about whether this is safe, but dotimes for instance says that

It is implementation-dependent whether dotimes establishes a new binding of var on each iteration or whether it establishes a binding for var once at the beginning and then assigns it on any subsequent iterations.

In other words

(dotimes (i n)
  ... my code ...)

might expand to something like

(let (...)
  ...
  (tagbody
   start
   (let ((i ...))
     ...my code...)
   (unless ... (go start))
   ...))

for instance.

I suspect pretty strongly that loop implementations don't do this in practice, and I think you probably could argue that the spec implies they are not allowed to do it, but I also think that the wording is not clear enough that you'd want to depend on that.

Secondly I think that as a matter of style adjusting the iteration variable's value inside the body of the iteration is, well, horrible: If someone reads (loop for i from 0 below 100 do ...) they expect i to step by integers from 0 to 99, not to jump about randomly because of some devious obscurity in the code. So I would personally avoid this as a matter of style rather than as a matter of being sure the spec allows or does not say it is safe.

Rather I would do what Rainer suggested, which is (loop for x = ... then ...) where it is clear that x has values which are determined by your code, not by loop.

Finally note that you can detect the iteration-variable-bound-on-each-iteration thing by, for instance:

> (mapcar #'funcall (loop for i below 3
                                    collect (lambda () i)))
(3 3 3)

In this case the implementation I am using does not, in this case rebind it (but it might do in other cases of course!).


It's also worth noting that the possibility of assignment to loop control variables prevents an important optimisation being possible: loop unrolling. If the system can't assume that (loop for i from 0 to 2 do ...) evaluates ... three times, with i being the appropriate value, you can't really unroll such a loop.

Itemized answered 3/1, 2020 at 17:46 Comment(1)
On the style issue, I would initialize the z variable completely outside of the loop to make it clear that z is not the loop iteration counter. (let ((z 0)) (loop ... do (incf z) ... (if ... (setf z 0)))). The with z = 0 is also OK imo, but for z from 1 would definitely confuse people.Titfer
S
6

Another possibility:

(loop for z = 0 then (if ... 0 (1+ z))
      ...
      )
Schmitt answered 3/1, 2020 at 17:8 Comment(0)
I
6

I think you should avoid doing so, for two reasons.

Firstly as far as I can see the loop specification makes no clear statement about whether this is safe, but dotimes for instance says that

It is implementation-dependent whether dotimes establishes a new binding of var on each iteration or whether it establishes a binding for var once at the beginning and then assigns it on any subsequent iterations.

In other words

(dotimes (i n)
  ... my code ...)

might expand to something like

(let (...)
  ...
  (tagbody
   start
   (let ((i ...))
     ...my code...)
   (unless ... (go start))
   ...))

for instance.

I suspect pretty strongly that loop implementations don't do this in practice, and I think you probably could argue that the spec implies they are not allowed to do it, but I also think that the wording is not clear enough that you'd want to depend on that.

Secondly I think that as a matter of style adjusting the iteration variable's value inside the body of the iteration is, well, horrible: If someone reads (loop for i from 0 below 100 do ...) they expect i to step by integers from 0 to 99, not to jump about randomly because of some devious obscurity in the code. So I would personally avoid this as a matter of style rather than as a matter of being sure the spec allows or does not say it is safe.

Rather I would do what Rainer suggested, which is (loop for x = ... then ...) where it is clear that x has values which are determined by your code, not by loop.

Finally note that you can detect the iteration-variable-bound-on-each-iteration thing by, for instance:

> (mapcar #'funcall (loop for i below 3
                                    collect (lambda () i)))
(3 3 3)

In this case the implementation I am using does not, in this case rebind it (but it might do in other cases of course!).


It's also worth noting that the possibility of assignment to loop control variables prevents an important optimisation being possible: loop unrolling. If the system can't assume that (loop for i from 0 to 2 do ...) evaluates ... three times, with i being the appropriate value, you can't really unroll such a loop.

Itemized answered 3/1, 2020 at 17:46 Comment(1)
On the style issue, I would initialize the z variable completely outside of the loop to make it clear that z is not the loop iteration counter. (let ((z 0)) (loop ... do (incf z) ... (if ... (setf z 0)))). The with z = 0 is also OK imo, but for z from 1 would definitely confuse people.Titfer
P
3

ANSI CL contains multiple instances of wording which have the clear interpretation that variables are bound once and stepped destructively.

As far as mutating the variables of "variable initialization and stepping clauses goes", this is something that conforming code can do. The ANSL CL standard makes it clear that variables are bound once and then stepped by assignment, and that clauses are processed in order, with only certain exceptions.

In particular, there is this text in 6.1.2.1 Iteration Control:

The for and as clauses iterate by using one or more local loop variables that are initialized to some value and that can be modified or stepped after each iteration. For these clauses, iteration terminates when a local variable reaches some supplied value or when some other loop clause terminates iteration. At each iteration, variables can be stepped by an increment or a decrement or can be assigned a new value by the evaluation of a form.

See the last bit: "can be assigned a new value by the evaluation of a form".

Your alternative code using with is a bit verbose on two counts. Incrementing a variable can be done with incf. Secondly, the do clause of loop takes multiple forms; no need for progn.

Thus, if we needed this alternative, we could at least have it like this:

(loop
    with z = 0
    ...
    do (incf z)
       ...
       (when ...
          (setq z 0)))

Or possibly with a when clause:

(loop
    with z = 0
    ...
    do (incf z)
       ...
    when (condition ...) do
      (setq z 0))
Purity answered 4/1, 2020 at 5:37 Comment(1)
I think you are right about this (which is sad, because it limits how aggressively loop can be optimised). But I suspect that the 'can be assigned a new value by the evaluation of a form' may refer to the second form in for x = <form> then <form> clause. However none of the wording around loop is really clear enough to be sure about anything I think.Itemized

© 2022 - 2024 — McMap. All rights reserved.