Skip to content

Commit

Permalink
Add a :test to numbers. Deprecate the non-keyword arguments.
Browse files Browse the repository at this point in the history
- Modify `numbers`, `numbers-down`, and `numbers-up`.
- Fix signalling an error when arguments which give directions conflict.
- Add more tests.
  • Loading branch information
okamsn committed Sep 24, 2023
1 parent 311e9b7 commit 36c2faf
Show file tree
Hide file tree
Showing 3 changed files with 216 additions and 65 deletions.
83 changes: 55 additions & 28 deletions doc/loopy-doc.org
Original file line number Diff line number Diff line change
Expand Up @@ -1178,23 +1178,23 @@ variants =numbers-up= and =numbers-down=.
#+findex: number
#+findex: numbering
#+findex: numbers
- =(numbers|nums VAR [START [END [STEP]]] &key KEYS)= :: Iterate
through numbers. =KEYS= is one or several of =from=, =upfrom=, =downfrom=,
=to=, =upto=, =downto=, =above=, =below=, and =by=.
- =(numbers|nums VAR &key KEYS)= :: Iterate through numbers. =KEYS= is one or
several of =from=, =upfrom=, =downfrom=, =to=, =upto=, =downto=, =above=,
=below=, =by=, and =test=.

This command also has the aliases =num=, =number=, and =numbering=.

The command =numbers= is used to iterate through numbers. For example,
=(numbers i :from 1 :to 10)= is similar to =(list i (number-sequence 1 10))=,
and =(numbers i 3)= is similar to =(set i 3 (1+ i))=.

For efficiency, =VAR= is initialized to the starting numeric value, not ~nil~,
and is updated at the end of each step of the loop. This can be overridden
using the =with= special macro argument, which can result in slower code.
For efficiency, _=VAR= is initialized to the starting numeric value_, not
~nil~, and is updated at the end of each step of the loop. This can be
overridden using the =with= special macro argument, which can result in slower
code.

To balance convenience and similarity to other commands, =numbers= has a
flexible argument list. In its most basic form, it uses no keywords and takes
a starting value and an ending value. The ending value is inclusive.
In its most basic form, =numbers= iterates from a starting value to an
inclusive ending value using the =:from= and =:to= keywords, respectively.

#+begin_src emacs-lisp
;; => (1 2 3 4 5)
Expand All @@ -1212,11 +1212,10 @@ variants =numbers-up= and =numbers-down=.
(collect i))
#+end_src

To specify the step size, one can use an optional third argument (like in
Python's ~range~) or the keyword =:by= (like in ~cl-loop~). The value of the
optional third argument can be positive or negative. /However/, in keeping
with ~cl-loop~, the value for =:by= should always be positive; other keyword
arguments then control whether the variable is incremented or decremented.
To specify the step size, one can use the keyword =:by=. Except when =:test=
is given, _the value for =:by= must be positive_. Other keyword arguments
(=:upfrom=, =:downfrom=, =:upto=, =:downto=, =:above=, and =:below=) control
whether the variable is incremented or decremented.

#+begin_src emacs-lisp
;; => (1 3 5)
Expand All @@ -1239,9 +1238,11 @@ variants =numbers-up= and =numbers-down=.

By default, the variable's value starts at 0 and increases by 1. To specify
whether the value should be increasing or decreasing when using the =:by=
keyword, one can use the keywords =:downfrom=, =:downto=, =:upfrom=, and
=:upto=. The keywords =:from= and =:to= don't by themselves specify a
direction.
keyword, one can use the keywords =:downfrom=, =:downto=, =:upfrom=, =:upto=,
=:above=, and =:below=. The keywords =:from= and =:to= don't by themselves
specify a direction, and they can be used with the keyword arguments that do
without conflict. Using arguments that contradict one another will signal an
error.

#+begin_src emacs-lisp
;; => (3 2 1)
Expand All @@ -1261,14 +1262,13 @@ variants =numbers-up= and =numbers-down=.
(loopy (numbers i :from 10 :downto 2 :by 2)
(collect i))

;; Produced code is not as efficient as above:
;; => (10 8 6 4 2)
(loopy (numbers i :from 10 :to 2 -2)
(collect i))

;; => (1 2 3 4 5 6 7)
(loopy (numbers i :from 1 :upto 7)
(collect i))

;; => Signals an error:
(loopy (numbers i :downfrom 10 :upto 20)
(collect i))
#+end_src

To specify an /exclusive/ ending value, use the keywords =:below= for
Expand All @@ -1279,7 +1279,7 @@ variants =numbers-up= and =numbers-down=.
(loopy (numbers i :from 1 :below 10)
(collect i))

;; Same as
;; Same as above:
(loopy (set i 1 (1+ i))
(while (< i 10))
(collect i))
Expand All @@ -1293,12 +1293,39 @@ variants =numbers-up= and =numbers-down=.
(collect i))
#+end_src

#+ATTR_TEXINFO: :tag Note
#+begin_quote
Because the ~loopy~ macro can't test the value of the step size ahead of time,
being more explicit by using the keyword parameters can produce faster code.
#+end_quote
If you do not know whether you will be incrementing or decrementing, you can
use the keyword argument =test=, whose value is a function that should return
a non-nil value if the loop should continue, such as ~#'<=~. The function
receives =VAR= as the first argument and the final value as the second
argument, as in ~(funcall TEST VAR FINAL-VAL)~. =test= can only be used with
=from= and =to=; it cannot be used with keywords that already describe a
direction and ending condition. To match the behavior of ~cl-loop~, the
default testing function is ~#'<=~. When =test= is given, =by= can be
negative.

#+begin_src emacs-lisp
;; => (10 9.5 9.0 8.5 8.0 7.5 7.0 6.5 6.0 5.5)
(loopy (with (start 10)
(end 5)
(func #'>)
(step -0.5))
(numbers i :to end :from start :by step :test func)
(collect i))

;; Expands to similar code as above.
;; Note that with `:above', step must be positive.
;;
;; => (10 9.5 9.0 8.5 8.0 7.5 7.0 6.5 6.0 5.5)
(loopy (with (start 10)
(end 5)
(step 0.5))
(numbers i :from start :above end :by step)
(collect i))

;; Signals an error because `:upto' implies a testing function already:
(loopy (numbers i :from 1 :upto 10 :test #'<)
(collect i))
#+end_src

If you prefer using positional arguments to keyword arguments, you can use the
commands =numbers-up= and =numbers-down= to specify directions. These commands
Expand Down
108 changes: 71 additions & 37 deletions loopy-commands.el
Original file line number Diff line number Diff line change
Expand Up @@ -636,24 +636,25 @@ The values are returned in a list in that order as a plist.
PLIST contains the keyword arguments passed to a sequence
iteration command. The supported keywords are:
- from, upfrom (inclusive start)
- downfrom (inclusive start)
- to, upto (inclusive end)
- downto (inclusive end)
- above (exclusive end)
- below (exclusive end)
- by (increment)"
- `:from', `:upfrom' (inclusive start)
- `:downfrom' (inclusive start)
- `:to', `:upto' (inclusive end)
- `:downto' (inclusive end)
- `:above' (exclusive end)
- `:below' (exclusive end)
- `:by' (increment)
- `:test' (comparison function)"

(loopy--plist-bind ( :from from :upfrom upfrom :downfrom downfrom
:to to :upto upto :downto downto
:above above :below below
:by by)
:by by :test test)
plist
;; Check the inputs:
(when (or (< 1 (cl-count-if #'identity (list from upfrom downfrom)))
(< 1 (cl-count-if #'identity (list to upto downto above below)))
(and downfrom below)
(and upfrom above))
(and (or downfrom downto above)
(or upfrom upto below)))
(signal 'loopy-conflicting-command-arguments (list plist)))

(let ((decreasing (or downfrom downto above)))
Expand All @@ -671,6 +672,8 @@ iteration command. The supported keywords are:
`(:by ,by))
,@(when-let ((end (or to downto above below upto)))
`(:end ,end))
:dir-given ,(or above below downfrom downto upfrom upto)
:test ,test
:decreasing ,decreasing
:inclusive ,(not (or above below))))))

Expand Down Expand Up @@ -996,36 +999,52 @@ map's keys. Duplicate keys are ignored."

;;;;;; Numbers
(loopy--defiteration numbers
"Parse the `numbers' command as (nums VAR [START [END [STEP]]] &key KEYS).
"Parse the `numbers' command as (nums VAR &key KEYS).
- START is the starting index, if given.
- END is the ending index (inclusive), if given.
- STEP is a positive or negative step size, if given.
KEYS is one or several of `:index', `:by', `:from', `:downfrom',
`:upfrom', `:to', `:downto', `:upto', `:above', or `:below'.
`:upfrom', `:to', `:downto', `:upto', `:above', `:below', or
`:test'.
- `:by' is the increment step size as a positive value.
- `:from', `:downfrom', and `:upfrom' name the starting index
- `:to', `:downto', and `:upto' name the ending index (inclusive)
- `:below' and `:above' name an exclusive ending index.
- `:test' is the function that checks whether the loop ends,
as in `(TEST VAR END)'. `:test' can only be used
when a direction is not already given.
`:downto' and `:downfrom' make the index decrease instead of increase."
:keywords (:by :from :downfrom :upfrom :to :downto :upto :above :below)
:keywords (:by :from :downfrom :upfrom :to :downto :upto :above :below :test)
:required-vals 0
:other-vals (0 1 2 3)
:instructions
;; TODO: `cl-destructuring-bind' signals error here. Why?
(seq-let (explicit-start explicit-end explicit-by)
other-vals
(loopy--plist-bind ( :start key-start :end key-end :by key-by
:decreasing decreasing :inclusive inclusive)
(loopy--find-start-by-end-dir-vals opts)
:decreasing decreasing :inclusive inclusive
:dir-given dir-given :test test)

(condition-case nil
(loopy--find-start-by-end-dir-vals opts)
(loopy-conflicting-command-arguments
(signal 'loopy-conflicting-command-arguments (list cmd))))

;; Warn that the non-keyword arguments are deprecated.
(when (or explicit-start
explicit-end
explicit-by)
(warn "`loopy': `numbers': The non-keyword arguments are deprecated.
Instead, use the keyword arguments, possibly including the new `:test' argument."))

;; Check that nothing conflicts.
(when (or (and explicit-start key-start)
(and explicit-end key-end)
(and explicit-by key-by))
(and explicit-end key-end)
(and explicit-by key-by))
(signal 'loopy-conflicting-command-arguments (list cmd)))

(let* ((end (or explicit-end key-end))
Expand All @@ -1039,41 +1058,56 @@ KEYS is one or several of `:index', `:by', `:from', `:downfrom',
(gensym "num-test-var")
var)))

(when (and test (or dir-given (null end)))
(signal 'loopy-conflicting-command-arguments (list cmd)))

`((loopy--iteration-vars (,var-val-holder ,start))
,(when (loopy--with-bound-p var)
`(loopy--main-body (setq ,var ,var-val-holder)))

(loopy--latter-body
(setq ,var-val-holder ,(let ((inc (if number-by by increment-val-holder)))
(cond (explicit-by `(+ ,var-val-holder ,inc))
(key-by `(,(if decreasing #'- #'+)
,var-val-holder ,inc))
(decreasing `(1- ,var-val-holder))
(t `(1+ ,var-val-holder))))))
(setq ,var-val-holder
,(let ((inc (if number-by
by
increment-val-holder)))
(cond (explicit-by `(+ ,var-val-holder ,inc))
(test `(+ ,var-val-holder ,inc))
(key-by `(,(if decreasing #'- #'+)
,var-val-holder ,inc))
(decreasing `(1- ,var-val-holder))
(t `(1+ ,var-val-holder))))))

,@(cond
(number-by-and-end
`((loopy--pre-conditions (,(if explicit-by
(if (cl-plusp by) #'<= #'>=)
(if inclusive
(if decreasing #'>= #'<=)
(if decreasing #'> #'<)))
,var-val-holder ,end))))
`((loopy--pre-conditions ,(loopy--apply-function
(cond
(test test)
(explicit-by
(if (cl-plusp by) '#'<= '#'>=))
(inclusive
(if decreasing '#'>= '#'<=))
(t (if decreasing '#'> '#'<)))
var-val-holder end))))
;; `end' is not a number. `by' might be a number.
(end
`((loopy--iteration-vars (,end-val-holder ,end))
,(when (and (not number-by)
(or key-by explicit-by))
`(loopy--iteration-vars (,increment-val-holder ,by)))
,@(cond
(test `((loopy--pre-conditions
,(loopy--apply-function
test var-val-holder end-val-holder))))
((not explicit-by) ; `key-by' or default
`((loopy--pre-conditions (,(if inclusive
(if decreasing #'>= #'<=)
(if decreasing #'> #'<))
,var-val-holder ,end-val-holder))))
`((loopy--pre-conditions ,(loopy--apply-function
(if inclusive
(if decreasing '#'>= '#'<=)
(if decreasing '#'> '#'<))
var-val-holder end-val-holder))))
(number-by
`((loopy--pre-conditions (,(if (cl-plusp by) #'<= #'>=)
,var-val-holder ,end-val-holder))))
`((loopy--pre-conditions ,(loopy--apply-function
(if (cl-plusp by) '#'<= '#'>=)
var-val-holder end-val-holder))))
;; Ambiguous, so need to check
(t
(let ((fn (gensym "nums-fn")))
Expand Down Expand Up @@ -1103,7 +1137,7 @@ This is for increasing indices.
(when (and by (cl-second other-vals))
(signal 'loopy-conflicting-command-arguments (list cmd)))
(loopy--parse-loop-command
`(numbers ,var ,val
`(numbers ,var :from ,val
:upto ,(cl-first other-vals)
:by ,(or by (cl-second other-vals))))))

Expand All @@ -1124,7 +1158,7 @@ This is for decreasing indices.
(when (and by (cl-second other-vals))
(signal 'loopy-conflicting-command-arguments (list cmd)))
(loopy--parse-loop-command
`(numbers ,var ,val
`(numbers ,var :from ,val
:downto ,(cl-first other-vals)
:by ,(or by (cl-second other-vals))))))

Expand Down
Loading

0 comments on commit 36c2faf

Please sign in to comment.