Skip to content

Commit 2376e9a

Browse files
authored
Remove initialization optimizations (#226)
This affects: - `cons` - `cycle` - `iter` - `numbers` - `seq-index` Remove optimizations that use iterations variables more directly by initializing them to the value expected during the first iteration step. Doing that can change behavior, as iteration variables could be updated again before the loop is expected to exit, which currently happens for `cl-loop`. ```emacs-lisp ;; => (4 (1 2 3)) (cl-loop for elem in (list 1 2 3) for i from 1 collect i into is finally return (list i is)) ``` Loopy currently copies that behavior, which could introduce unexpected results by default. For example, SBCL gives a different result. ```commonlisp ;; => (3 (1 2 3)) (loop for elem in (list 1 2 3) for num from 1 collect num into nums finally (return (list num nums))) ``` Making this optimization optional (as we currently do via use of the `with` special macro argument) means that users would have to remember the internal implementations of each command with optimization to remember how they change. See also #204. If one were to add an `iter-opt` special macro argument and change default behavior to be expected (more like SBCL), that would only move the burden to needing to remember when to use `iter-opt`. As mention in #204, using `iter-opt` requires the user to: 1. Read the documentation to know that a command supports it 2. Always remember that the command supports it while using the macro 3. Always remember how it affects the command while using it all for a slight gain in speed and marginally less typing. For those reasons, instead of adding `iter-opt`, it is better to remove these few optimized behaviors in favor of more standard behavior. As noted in #204, instead of writing ```emacs-lisp ;; => (5 (1 2 3 4)) (loopy (iter-opt (numbers i)) (list elem '(1 2 3 4)) (numbers i :from 1 :to 10) (collect i) (finally-return i loopy-result)) ``` one could instead write ```emacs-lisp ;; => (5 (1 2 3 4)) (loopy (with (i 1)) (list elem '(1 2 3 4)) (while (<= i 10)) (collect i) (set i (1+ i)) (finally-return i loopy-result)) ``` which is basically the same length while requiring less knowledge of each optimized iteration command's internals. There are already cases where Loopy is not hyper-optimized, such as explicit accumulation variables. However, there, the behavior of `accum-opt` is much less of a burden to remember, because it affects every accumulation command in the same way. There are no special cases to remember. For these optimizations, it is the case that Loopy's flexibility and consistency has a small cost, but they are not the only case of that (consider how `cl-loop` moves some variable setting to `and` expressions while we settle for `catch` and `throw`) and we do not question the value of consistency and predictability in those cases.
1 parent 7e84ba5 commit 2376e9a

File tree

6 files changed

+389
-207
lines changed

6 files changed

+389
-207
lines changed

CHANGELOG.md

+42
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,46 @@ version is needed for generic sequences.
7272
- Allow the `unique` keyword argument of the commands `map` and `map-ref` to be
7373
evaluable at run time, instead of just checked at compile time ([#209]).
7474

75+
- Remove the initialization optimizations that produced faster code but
76+
could change final results ([#226, #204]). For example, consider the
77+
difference results between `cl-loop` and SBCL's `loop`:
78+
79+
``` emacs-lisp
80+
;; => (4 (1 2 3))
81+
(cl-loop for elem in (list 1 2 3)
82+
for i from 1
83+
collect i into is
84+
finally return (list i is))
85+
```
86+
87+
88+
``` common-lisp
89+
;; => (3 (1 2 3))
90+
(loop for elem in (list 1 2 3)
91+
for num from 1
92+
collect num into nums
93+
finally (return (list num nums)))
94+
```
95+
96+
Loopy would give the same result as `cl-loop` when using the optimization and
97+
would given the same result as SBCL's `loop` when not using the optimization.
98+
99+
Working around having different results based on unstated (though documented)
100+
settings would mean requiring users to fully know the implementations of each
101+
loop command and how they could change when optimized. That position also
102+
argues against making use of the optimization more explicit via an added
103+
`iter-opt` special macro argument, as discussed in [#226] and [#204].
104+
105+
Therefore, these optimizations are being removed and Loopy is reverting to its
106+
previous behavior of initializing the iteration variables to `nil` by default
107+
for the following commands:
108+
- `cons`
109+
- `cycle`
110+
- `iter`
111+
- `numbers`
112+
- `seq-index`
113+
- `substream`
114+
75115
### Improvements
76116

77117
- The `map` and `map-ref` commands now check for duplicate keys step by step,
@@ -107,6 +147,7 @@ version is needed for generic sequences.
107147
[#179]: https://github.com/okamsn/loopy/issues/179
108148
[#184]: https://github.com/okamsn/loopy/issues/184
109149
[#203]: https://github.com/okamsn/loopy/pull/203
150+
[#204]: https://github.com/okamsn/loopy/issues/204
110151
[#205]: https://github.com/okamsn/loopy/pull/205
111152
[#206]: https://github.com/okamsn/loopy/pull/206
112153
[#207]: https://github.com/okamsn/loopy/pull/207
@@ -117,6 +158,7 @@ version is needed for generic sequences.
117158
[#213]: https://github.com/okamsn/loopy/pull/213
118159
[#215]: https://github.com/okamsn/loopy/pull/215
119160
[#217]: https://github.com/okamsn/loopy/pull/217
161+
[#226]: https://github.com/okamsn/loopy/pull/226
120162

121163
## 0.13.0
122164

README.org

+4
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ please let me know.
5353
beginning of the loop.
5454
- The =:on-failure= argument of the =find= command is now evaluated at the
5555
beginning of the loop.
56+
- Changed back to the old, slightly slower behavior of always initializing
57+
iteration variables to ~nil~, instead of sometimes initializing to the
58+
expected value during the first iteration step. This affects =cons=,
59+
=cycle=, =iter=, =numbers=, =seq-index=, and =substream=.
5660
- Version 0.13.0:
5761
- The deprecated =:init= keyword argument has been removed. Use the =with=
5862
special macro argument instead.

doc/loopy-doc.org

+123-40
Original file line numberDiff line numberDiff line change
@@ -1436,14 +1436,6 @@ In ~loopy~, iteration commands are named after what they iterate through. For
14361436
example, the =array= and =list= commands iterate through the elements of arrays
14371437
and lists, respectively.
14381438

1439-
#+ATTR_TEXINFO: :tag Note
1440-
#+begin_quote
1441-
In general, iteration variables (such as the ~i~ and ~j~ above) are initialized
1442-
to ~nil~. For efficiency, some commands do not do this. In such cases, the
1443-
initial value of an iteration variable can be set using the =with= special macro
1444-
argument, but this can result in less efficient code.
1445-
#+end_quote
1446-
14471439
Because some iteration commands use their variable to manage state, it is an
14481440
error to use the same iteration variable for multiple iteration commands.
14491441

@@ -1454,6 +1446,85 @@ error to use the same iteration variable for multiple iteration commands.
14541446
(finally-return t))
14551447
#+end_src
14561448

1449+
Iteration variables are initialized to ~nil~ and they are updated at the point
1450+
in the loop body corresponding to the loop command's position in the macro's
1451+
arguments.
1452+
1453+
#+begin_src emacs-lisp
1454+
;; `elem' retains its value from the previous
1455+
;; iteration until it is updated again:
1456+
;;
1457+
;; => (((1 . nil) ; before
1458+
;; (2 . 1)
1459+
;; (3 . 2)
1460+
;; (4 . 3))
1461+
;; ((1 . 1) ; after
1462+
;; (2 . 2)
1463+
;; (3 . 3)
1464+
;; (4 . 4)))
1465+
(loopy (numbers nth :from 1)
1466+
(collect elem-before (cons nth elem))
1467+
(list elem '(1 2 3 4))
1468+
(collect elem-after (cons nth elem))
1469+
(finally-return elem-before
1470+
elem-after))
1471+
#+end_src
1472+
1473+
Be aware that ~cl-loop~ does not consistently initialize its iteration variables
1474+
to nil. For some of ~cl-loop~'s iteration (=for=) statements, the variable is
1475+
initialized to its value for the first iteration step and is manipulated
1476+
directly at the end of the iteration step. Loopy avoids this, as seen in the
1477+
below example, but that can result in unnecessary indirection for some use
1478+
cases, which has a minor speed cost.
1479+
1480+
#+begin_src emacs-lisp
1481+
;; => (5 (1 2 3 4) (1 2 3 4))
1482+
(cl-loop for elem in (list 1 2 3 4)
1483+
collect num into nums-1
1484+
for num from 1
1485+
collect num into nums-2
1486+
finally return (list num nums-1 nums-2))
1487+
1488+
;; => (4 (nil 1 2 3) (1 2 3 4))
1489+
(loopy (list elem (list 1 2 3 4))
1490+
(collect nums-1 num)
1491+
(numbers num :from 1)
1492+
(collect nums-2 num)
1493+
(finally-return num nums-1 nums-2))
1494+
#+end_src
1495+
1496+
Generally, iteration commands with conditions check whether to terminate the
1497+
loop /before/ the next iteration is run. They do not check their conditions
1498+
while running the current iteration step. In the below example, note that the
1499+
final value of ~i~ is 2 and not 3, even though the =do= command (similar to
1500+
~cl-loop~'s =do= keyword) is placed before the =list= command. Even though ~i~
1501+
is updated before ~elem~ is updated, the decision whether to continue the loop,
1502+
based on the =list= command's condition, is made /before/ the code in the =do=
1503+
command is run.
1504+
1505+
#+begin_src emacs-lisp
1506+
;; => 2, not 3
1507+
(let ((i 0))
1508+
(loopy (do (setq i (1+ i)))
1509+
(list elem '(0 1)))
1510+
i)
1511+
#+end_src
1512+
1513+
If you do wish to conditionally leave the loop during an iteration, consider
1514+
using the =leave= and =leave-from= commands ([[#exiting-the-loop-early]]).
1515+
1516+
#+begin_src emacs-lisp
1517+
;; => (3 (0 1))
1518+
(loopy (with (some-list (list 0 1))
1519+
(i 0))
1520+
(do (setq i (1+ i)))
1521+
(when (null some-list)
1522+
(leave))
1523+
(collect elems (car some-list))
1524+
(do (setq some-list (cdr some-list)))
1525+
(finally-return i elems))
1526+
#+end_src
1527+
14571528
Unlike ~cl-loop~ and like Common Lisp's ~iterate~, arguments of the iteration
14581529
commands are evaluated only once. For example, while iterating through numbers,
14591530
you can't suddenly change the direction of the iteration in the middle of the
@@ -1472,12 +1543,15 @@ loop. This restriction allows for producing more efficient code.
14721543
#+findex: cycling
14731544
#+findex: repeat
14741545
#+findex: repeating
1475-
- =(cycle|repeat [VAR] EXPR)= :: Run the loop for =EXPR= iterations. If
1476-
specified, =VAR= starts at 0, and is incremented by 1 at the end of each step
1477-
in the loop. If =EXPR= is 0, then the loop isn't run.
1546+
- =(cycle|repeat [VAR] EXPR)= :: Run the loop for =EXPR= iterations.
1547+
1548+
If given, then during the loop, =VAR= is set to the number of iteration steps
1549+
that have been run (0 for the first iteration step).
1550+
1551+
If =EXPR= is 0, then the loop isn't run.
14781552

1479-
For efficiency, =VAR= is not initialized to ~nil~. This can be overridden
1480-
using the =with= special macro argument, which can result in slower code.
1553+
=(cycle VAR EXPR)= works the same as =(numbers VAR :from 0 :below EXPR)=
1554+
([[#numeric-iteration]]).
14811555

14821556
This command also has the aliases =cycling= and =repeating=.
14831557

@@ -1493,6 +1567,14 @@ loop. This restriction allows for producing more efficient code.
14931567
(collect i)
14941568
(collect j))
14951569

1570+
;; Same as above:
1571+
;;
1572+
;; => (10 0 10 1 10 2)
1573+
(loopy (with (i 10))
1574+
(numbers j :from 0 :below 3)
1575+
(collect i)
1576+
(collect j))
1577+
14961578
;; An argument of 0 stops the loop from running:
14971579
;; => nil
14981580
(loopy (cycle 0)
@@ -1519,13 +1601,6 @@ loop. This restriction allows for producing more efficient code.
15191601
once, =yield-result= is an expression which is substituted into the loop body.
15201602
Therefore, =yield-result= can be used to repeatedly call functions.
15211603

1522-
For efficiency, when possible, =VAR= is bound to the yielded value before each
1523-
step of the loop, which is used to detect whether the iterator signals that it
1524-
is finished. This is not possible when destructuring. You can override this
1525-
behavior by using the =with= special macro argument, which can result in
1526-
slower code and tells the macro that the initial value of =VAR= is meaningful
1527-
and to update =VAR= during the loop.
1528-
15291604
This command also has the name =iterating=.
15301605

15311606
#+begin_src emacs-lisp
@@ -1632,11 +1707,6 @@ variants =numbers-up= and =numbers-down=.
16321707
=(list i (number-sequence 1 10))=, and =(numbers i 3)= is similar to
16331708
=(set i 3 (1+ i))=.
16341709

1635-
For efficiency, _=VAR= is initialized to the starting numeric value_, not
1636-
~nil~, and is updated at the end of each step of the loop. This can be
1637-
overridden using the =with= special macro argument, which can result in slower
1638-
code.
1639-
16401710
In its most basic form, =numbers= iterates from a starting value to an
16411711
inclusive ending value using the =:from= and =:to= keywords, respectively.
16421712

@@ -1646,6 +1716,34 @@ variants =numbers-up= and =numbers-down=.
16461716
(collect i))
16471717
#+end_src
16481718

1719+
Unlike ~cl-loop~, =VAR= is not initialized to the starting value given.
1720+
Instead, =VAR= is updated during the loop, like in other iteration
1721+
commands. This avoids unexpectedly changing the value of =VAR= after the
1722+
iteration step, as happens with some implementations of Common Lisp's ~loop~
1723+
macro (such ~cl-loop~).
1724+
1725+
#+begin_src emacs-lisp
1726+
;; => (4 (1 2 3 4))
1727+
(loopy (list elem (list 1 2 3 4))
1728+
(numbers num :from 1)
1729+
(collect nums num)
1730+
(finally-return num nums))
1731+
1732+
;; => (5 (1 2 3 4))
1733+
(cl-loop for elem in (list 1 2 3 4)
1734+
for num from 1
1735+
collect num into nums
1736+
finally return (list num nums))
1737+
1738+
;; SBCL returns 4, not 5:
1739+
;;
1740+
;; => (4 (1 2 3 4))
1741+
(loop for elem in (list 1 2 3 4)
1742+
for num from 1
1743+
collect num into nums
1744+
finally (return (list num nums)))
1745+
#+end_src
1746+
16491747
If the ending value is not given, then the value is incremented by 1 without
16501748
end.
16511749

@@ -1929,11 +2027,6 @@ source sequences.
19292027
- =(cons|conses VAR EXPR &key by)= :: Loop through the cons cells of =EXPR=.
19302028
Optionally, find the cons cells via the function =by= instead of =cdr=.
19312029

1932-
For efficiency, when possible, =VAR= is initialized to the value of =EXPR=,
1933-
not ~nil~, and is updated at the end of each step in the loop. This is not
1934-
possible when destructuring. Such initialization can be overridden by using
1935-
the =with= special macro argument, which can result in slower code.
1936-
19372030
This command also has the alias =consing=.
19382031

19392032
#+BEGIN_SRC emacs-lisp
@@ -2214,11 +2307,6 @@ source sequences.
22142307
and are compatible with features from the built-in =seq= library, such as
22152308
~seq-elt~ and ~seq-do~.
22162309

2217-
For efficiency, when possible, =VAR= is initialized to the value of =EXPR=,
2218-
not ~nil~, and is updated at the end of each step in the loop. This is not
2219-
possible when destructuring. Such initialization can be overridden by using
2220-
the =with= special macro argument, which can result in slower code.
2221-
22222310
Sub-streams can only be destructured using the =&seq= feature of the default
22232311
destructuring method ([[#basic-destructuring][Basic Destructuring]]), or by using the =seq= flag
22242312
([[#flags][Using Flags]]). Streams are neither lists nor arrays.
@@ -2313,11 +2401,6 @@ iterate.
23132401
With =numbers=, one would first need to explicitly calculate the length of the
23142402
sequence.
23152403

2316-
Similar to =numbers=, for efficiency, =VAR= is initialized to the starting
2317-
index value, not ~nil~, and is updated at the end of each step of the loop.
2318-
This can be overridden using the =with= special macro argument, which can
2319-
result in slower code.
2320-
23212404
#+begin_src emacs-lisp
23222405
;; => (97 98 99 100 101 102)
23232406
(loopy (with (my-string "abcdef"))

0 commit comments

Comments
 (0)