loopy 
- Description
- A looping macro
- Latest
- loopy-0.14.0.0.20250215.214626.tar (.sig), 2025-Feb-16, 1.09 MiB
- Maintainer
- Website
- https://github.com/okamsn/loopy
- Browse ELPA's repository
- CGit or Gitweb
- Badge
- Manual
- loopy
To install this package from Emacs, use package-install
or list-packages
.
Full description
loopy
is a macro meant for iterating and looping. It is similar in usage to
cl-loop
but uses symbolic expressions rather than keywords.
For most use cases, loopy
should be a nice substitute for cl-loop
and
complementary to the features provided by the Seq and CL libraries and Emacs's
regular looping and mapping features.
For detailed information, see the documentation file. This README is just an overview.
See also the extension package Loopy Dash, which provides destructuring using
the library dash.el
and is described in more detail below.
NOTE: Loopy is still in its latter middle stages.
Constructive criticism is welcome. If you see a place for improvement,
please let me know.
Recent breaking changes:
- Unreleased:
set
now warns when it is not given a value. In the future, it will signal an error.
- Version 0.14.0:
- Conflicting initialization values for accumulation variables now signal a warning. In the future, they will signal an error.
- The positional arguments to the
numbers
command have been removed, being deprecated since version 0.12.0. - Some built-in aliases have been made obsolete and will be removed from the
list of built-in aliases in the future. They can still be added to the
list of known aliases using
loopy-defalias
. See the changelog for more information. seq
andseq-ref
now work on generic sequences and are separate commands fromsequence
andsequence-ref
.sequence-index
now usesseq-length
.- Improved consistency of some keyword arguments:
- The
:unique
keyword argument of themap
andmap-ref
commands can now be evaluable at run time. - The
:close
argument of theiter
command is now evaluable, instead of only being used during macro expansion. - The
:close
argument of theiter
command is now evaluated at the beginning of the loop. - The
:on-failure
argument of thefind
command is now evaluated at the beginning of the loop.
- The
- Changed back to the old, slightly slower behavior of always initializing
iteration variables to
nil
, instead of sometimes initializing to the expected value during the first iteration step. This affectscons
,cycle
,iter
,numbers
,seq-index
, andsubstream
.
- See the change log for less recent changes.
1. Introduction
The loopy
macro is used to generate code for a loop, similar to cl-loop
.
Unlike cl-loop
, loopy
uses parenthetical expressions instead of "clauses".
;; A simple usage of `cl-loop': (cl-loop for i from 1 to 10 if (cl-evenp i) collect i into evens else collect i into odds end ; This `end' keyword is optional here. finally return (list odds evens)) ;; How it could be done using `loopy': (loopy (numbers i :from 1 :to 10) (if (cl-evenp i) (collect evens i) (collect odds i)) (finally-return odds evens)) (loopy (numbers i :from 1 :to 10) (if (cl-evenp i) (collect i :into evens) (collect i :into odds)) (finally-return odds evens))
loopy
supports destructuring for iteration commands like list
and
accumulation commands like sum
or collect
.
;; Summing the nth elements of arrays: ;; => (8 10 12 14 16 18) (loopy (list (list-elem1 list-elem2) '(([1 2 3] [4 5 6]) ([7 8 9] [10 11 12]))) (sum [sum1 sum2 sum3] list-elem1) (sum [sum4 sum5 sum6] list-elem2) (finally-return sum1 sum2 sum3 sum4 sum5 sum6)) ;; Or, more simply: ;; => (8 10 12 14 16 18) (loopy (list list-elem '(([1 2 3] [4 5 6]) ([7 8 9] [10 11 12]))) (sum ([sum1 sum2 sum3] [sum4 sum5 sum6]) list-elem) (finally-return sum1 sum2 sum3 sum4 sum5 sum6)) ;; Separate the elements of sub-list: ;; => ((1 3) (2 4)) (loopy (list i '((1 2) (3 4))) (collect (elem1 elem2) i) (finally-return elem1 elem2))
The loopy
macro is configurable and extensible. In addition to writing one's
own "loop commands" (such as list
in the example above), by using "flags", one
can choose whether to instead use pcase-let
, seq-let
, or even the Dash
library for destructuring.
;; Use `pcase' to destructure array elements: ;; => ((1 2 3 4) (10 12 14) (11 13 15)) (loopy (flag pcase) (array (or `(,car . ,cdr) digit) [1 (10 . 11) 2 (12 . 13) 3 4 (14 . 15)]) (if digit (collect digits digit) (collect cars car) (collect cdrs cdr)) (finally-return digits cars cdrs)) ;; Using the default destructuring: ;; => ((1 2 3 4) (10 12 14) (11 13 15)) (loopy (array elem [1 (10 . 11) 2 (12 . 13) 3 4 (14 . 15)]) (if (numberp elem) (collect digits elem) (collect (cars . cdrs) elem)) (finally-return digits cars cdrs))
Variables like cars
, cdrs
, and digits
in the example above are
automatically let
-bound so as to not affect code outside of the loop.
loopy
has arguments for binding (or not binding) variables, executing code
before or after the loop, executing code only if the loop completes, and for
setting the macro's return value (default nil
). This is in addition to the
looping features themselves.
All of this makes loopy
a useful and convenient choice for looping and
iteration.
2. Similar Libraries
Loopy is not the only Lisp library that uses parenthetical expressions instead of
keyword clauses (as in cl-loop
). Iterate and For are two examples from
Common Lisp.
;; Collecting 10 random numbers: ;; cl-loop (Emacs Lisp) (cl-loop repeat 10 collect (random 10)) ;; loopy (Loopy) (loopy (repeat 10) (collect (random 10))) ;; iterate (Common Lisp) (iterate (repeat 10) (collect (random 10))) ;; for (Common Lisp) (for:for ((i repeat 10) (randoms collecting (random 10))))
Generally, all of the packages handle basic use cases in similar ways. One
large difference is that iterate
can embed its looping constructs in arbitrary
code. Loopy is currently provides this feature as a separate macro,
loopy-iter
, which expands looping constructs using macroexpand
.
(require 'loopy-iter) ;; Things to node: ;; - `accum-opt' produces more efficient accumulations for names variables ;; - `cycling' is another name for `repeat' ;; => ((-9 -8 -7 -6 -5 -4 -3 -2 -1) ;; (0) ;; (1 2 3 4 5 6 7 8 9 10 11)) (loopy-iter (accum-opt positives negatives zeroes) (numbering i :from -10 :to 10) ;; Normal `let' and `pcase', not Loopy constructs: (let ((var (1+ i))) (pcase var ((pred cl-plusp) (collecting positives var)) ((pred cl-minusp) (collecting negatives var)) ((pred zerop) (collecting zeroes var)))) (finally-return negatives zeroes positives))
Loopy is not yet feature complete. Please request features or report problems
in this project’s issues tracker. While basic uses are covered, some of the
more niche features of cl-loop
and iterate
are still being added.
3. How to Install
Loopy can be installed from Non-GNU ELPA and MELPA as the package loopy
. The
optional package loopy-dash
can be installed to enable using the Dash library
for destructuring (instead of other methods).
(use-package loopy) ;; Optional support for destructuring with Dash. (use-package loopy-dash :after (loopy) :demand t)
To load all of the alternative destructuring libraries (see section Multiple Kinds of Destructuring) and the alternative macro form (see section Loop Commands in Arbitrary Code), use
(use-package loopy :config (require 'loopy-iter) (require 'loopy-pcase) (require 'loopy-seq)) (use-package loopy-dash :after (loopy) :demand t)
4. Multiple Kinds of Destructuring
The default destructuring system is a super-set of what cl-lib
provides
and is described in the section Basic Destructuring in the documentation.
In addition to the built-in destructuring style, loopy
can optionally use
destructuring provided by pcase-let
, seq-let
, and the dash
library. This
provides greater flexibility and allows you to use destructuring patterns that
you're already familiar with.
These features can be enabled with "flags", described in the section Using Flags in the documentation.
Here are a few examples that demonstrate how loopy
can use destructuring with
accumulation commands.
(require 'loopy-dash) ;; => (((1 (2 3)) (4 (5 6))) ; whole ;; (1 4) ; i ;; (3 6)) ; k (loopy (flag dash) (list elem '((1 (2 3)) (4 (5 6)))) (collect (whole &as i (_ k)) elem) (finally-return whole i k)) ;; = > ((3 5) (4 6)) (loopy (flag dash) (list (&plist :a a :b b) '((:a 3 :b 4 :c 7) (:g 8 :a 5 :b 6))) (collect a-vals a) (collect b-vals b) (finally-return a-vals b-vals)) (require 'loopy-pcase) ;; => ((1 4) (3 6)) (loopy (flag pcase) (list elem '((1 (2 3)) (4 (5 6)))) (collect `(,a (,_ ,b)) elem) (finally-return a b)) ;; => ((1 6) (3 8) ([4 5] [9 10])) (require 'loopy-seq) (loopy (flag seq) (list elem '([1 2 3 4 5] [6 7 8 9 10])) (collect [a _ b &rest c] elem) (finally-return a b c))
For more on how dash
does destructuring, see their documentation on the -let
expression.
5. Loop Commands in Arbitrary Code
The macro loopy-iter
can be used to embed loop commands in arbitrary code. It
is similar in use to Common Lisp's Iterate macro, but it is not a port of
Iterate to Emacs Lisp.
(require 'loopy-iter) ;; => ((1 2 3) (-3 -2 -1) (0)) (loopy-iter (accum-opt positives negatives other) (numbering i :from -3 :to 3) (pcase i ((pred cl-plusp) (collecting positives i)) ((pred cl-minusp) (collecting negatives i)) (_ (collecting other i))) (finally-return positives negatives other)) ;; => 6 (loopy-iter (listing elem '(1 2 3)) (funcall #'(lambda (x) (summing x)) elem))
For more on this, see the documentation.
6. Adding Custom Commands
It is easy to create custom commands for Loopy. To see how, see the section Custom Commands in the documentation.
7. Comparing to cl-loop
See the documentation page Comparing to cl-loop
. See also the wiki page Speed
Comparisons.
8. Real-World Examples
See the wiki page Examples.
Old versions
loopy-0.14.0.0.20250208.155652.tar.lz | 2025-Feb-08 | 166 KiB |
loopy-0.14.0.0.20250119.13540.tar.lz | 2025-Feb-01 | 166 KiB |
News
CHANGELOG
This document describes the user-facing changes to Loopy.
For Loopy Dash, see https://github.com/okamsn/loopy-dash.
Unreleased
Breaking Changes
set
now warns when it is not given a value ([#229]). Currently,(set VAR)
bindsVAR
tonil
, but since this form is indistinguishable from a mistake, and sincenil
is a short word to write, this behavior is deprecated.
0.14.0
Commands for Generic (seq.el
) Sequences
Loopy can now loop through generic sequences implemented by the library seq.el
([#215], [#150], [#136]). Currently, this is done naively via the functions
seq-elt
and seq-length
. Because a package might implement a generic
sequence using one of the built-in sequence types (lists and arrays), no attempt
is made to optimize behavior for particular kinds of sequences. As a
comparison, the sequence
command gives special consideration to lists in some
circumstances.
Because these commands use seq-length
, they do not work with infinite
sequences. For that, consider using the stream
command.
The new commands are seq
and seq-ref
, which were previously aliases of
sequence
and sequence-ref
, respectively ([#126, #206]). This change should
not cause an error, but the expanded code might be slower depending on the type
of the sequence.
sequence-index
, which keeps the alias seq-index
, has been changed to use
seq-length
instead of length
. This command is simple enough that no special
version is needed for generic sequences.
Breaking Changes
Conflicting starting values for accumulation variables, such as from using
sum
(value: 0) andmultiply
(value: 1), now signal an error ([#169], [#203]). In the future, they will signal an error.For now, Loopy continues the behavior of using the first found starting value from the macro arguments. To silence this warning and to avoid this future error, use the
with
special macro argument to explicitly state a starting value for the accumulation variable.Remove the deprecated positional arguments to the
numbers
command ([#205]).To cut back on an over-abundance of choice and to simplify documentation, the following built-in aliases have been made obsolete ([#126], [#168], [#206], [#207]). They can still be added manually via
loopy-defalias
.array
:across
array-ref
:arrayf
,arrayingf
,stringf
,stringingf
,across-ref
command-do
:group
cons
:on
list
:in
list-ref
:listf
,listingf
,in-ref
map-ref
:mapf
,mappingf
numbers
:num
,nums
numbers-down
:nums-down
,numdown
,num-down
,numsdown
numbers-up
:nums-up
,numup
,num-up
,numsup
set
:expr
,exprs
set-prev
:prev
,prev-expr
sequence
:elements
sequence-index
:sequencei
,seqi
,listi
,arrayi
,stringi
sequence-ref
:sequencef
,sequencingf
,elements-ref
seq-ref
:seqf
,seqingf
Review when the values of keyword arguments are taken and used ([#210]):
- Make the
close
keyword argument of theiter
command able to be evaluated during run time ([#211]). It was previously only used during macro expansion. - Make the
close
keyword argument of theiter
command evaluated at the start of the loop to be consistent with other commands ([#211]). - Make the
on-failure
keyword argument of thefind
command evaluated at the start of the loop to be consistent with other commands ([#211]). - Allow the
unique
keyword argument of the commandsmap
andmap-ref
to be evaluable at run time, instead of just checked at compile time ([#209]).
- Make the
Remove the initialization optimizations that produced faster code but could change final results ([#226, #204]). For example, consider the difference results between
cl-loop
and SBCL'sloop
:``` emacs-lisp ;; => (4 (1 2 3)) (cl-loop for elem in (list 1 2 3) ... ...