loopy
loopy copied to clipboard
A looping and iteration macro.
#+title: Loopy: A Looping and Iteration Macro
Make sure to export all headings as such. Otherwise, some links to
sub-headings won’t work.
#+options: H:6
Some parsers require this option to export footnotes.
#+options: f:t
MELPA Badges
=loopy=: [[https://melpa.org/#/loopy][file:https://melpa.org/packages/loopy-badge.svg]] \ =loopy-dash=: [[https://melpa.org/#/loopy-dash][file:https://melpa.org/packages/loopy-dash-badge.svg]]
~loopy~ is a macro meant for iterating and looping. It is similar in usage to [[https://www.gnu.org/software/emacs/manual/html_node/cl/Loop-Facility.html#Loop-Facility][~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 [[https://www.gnu.org/software/emacs/manual/html_node/elisp/Sequences-Arrays-Vectors.html][Seq]] and [[https://www.gnu.org/software/emacs/manual/html_node/cl/index.html][CL]] libraries and Emacs's regular looping and mapping features.
For detailed information, see [[file:doc/loopy-doc.org][the documentation file]]. This README is just an overview.
#+begin_center NOTE: Loopy is still in its latter middle stages.\ Constructive criticism is welcome. If you see a place for improvement, please let me know. #+end_center
Recent breaking changes:
- Unreleased:
- The deprecated =:init= keyword argument has been removed. Use the =with= special macro argument instead.
- The deprecated =:result-type= keyword argument has been removed. Use the =finally-return= special macro argument instead in combination with ~cl-coerce~, ~seq-into~, or a similar function.
- The commands =always=, =never=, and =thereis= now have the signature =(command [VAR] CONDITION &key into)=, similar to accumulation commands. They no longer take multiple condition arguments.
- Version 0.12.0:
- The boolean commands =always=, =never=, and =thereis= now behave more like accumulation commands and use ~loopy-result~ by default.
- Using multiple conditions in =always=, =never=, and =thereis= is deprecated. These commands will be changed to have call argument lists more like accumulation commands, such as =(always [VAR] VAL &key into)=. This will simplify the code and remove an inconsistency between them and the other commands.
- Re-using iteration variables in multiple iteration commands now signals an error. It never produced correct code.
- By default, the commands =cons=, =iter=, =nums=, and =seq-index= /can/ update named iteration variables outside of the main loop body and initialize variables to non-nil values, producing faster code. This can be overridden via the special macro argument =with=.
- =:result-type= is deprecated. Instead, use coercion functions in special macro arguments, possibly with =accum-opt=.
- The =:init= keyword argument is deprecated. Use the special macro argument =with= instead.
- =reduce= has been fixed. It now works like ~cl-reduce~ when the variable starting value isn't explicitly given, storing the first value instead of storing the result of passing the first value and ~nil~ to the function.
- The deprecated flags =lax-naming= and =split= were removed.
- The deprecated command =sub-loop= was removed.
- The non-keyword arguments of =numbers= are deprecated. Use the keyword arguments instead. A =:test= keyword argument was added, which is more flexible and explicit than the non-keyword arguments.
- Fixed a problem with macro expansion in some cases for sub-macros that created a new macro environment (e.g., ~cl-flet~).
- Loop commands now evaluate arguments like =:test=, =:key=, and =:by= only once.
- Change the argument order of =test= to be (1) the sequence element then (2) the tested value, like in ~seq-contains-p~ and unlike in ~cl-member~, and explicitly state this order in the documentation.
- =&key= now signals an error when there are unmatched keys in the plist, as in =cl-lib=. =&allow-other-keys= has been added.
- See the [[https://github.com/okamsn/loopy/blob/master/CHANGELOG.md][change log]] for less recent changes.
This auto-generated by toc-org.
- Table of Contents :TOC:noexport:
- [[#introduction][Introduction]]
- [[#similar-libraries][Similar Libraries]]
- [[#how-to-install][How to Install]]
- [[#multiple-kinds-of-destructuring][Multiple Kinds of Destructuring]]
- [[#loop-commands-in-arbitrary-code][Loop Commands in Arbitrary Code]]
- [[#adding-custom-commands][Adding Custom Commands]]
- [[#comparing-to-cl-loop][Comparing to =cl-loop=]]
- [[#real-world-examples][Real-World Examples]]
- 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".
#+begin_src emacs-lisp
;; 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)) #+end_src
~loopy~ supports destructuring for iteration commands like =list= and accumulation commands like =sum= or =collect=.
#+begin_src emacs-lisp ;; 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)) #+end_src
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.
#+begin_src emacs-lisp
;; 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)) #+end_src
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.
- Similar Libraries
Loopy is not the only Lisp library that uses parenthetical expressions instead of keyword clauses (as in ~cl-loop~). [[https://common-lisp.net/project/iterate/][Iterate]] and [[https://github.com/Shinmera/for/][For]] are two examples from Common Lisp.
#+begin_src emacs-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))))
#+end_src
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~.
#+begin_src emacs-lisp (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))
#+end_src
Loopy is not yet feature complete. Please request features or report problems in this project’s [[https://github.com/okamsn/loopy/issues][issues tracker]]. While basic uses are covered, some of the more niche features of ~cl-loop~ and ~iterate~ are still being added.
- How to Install
Loopy can be installed from [[https://melpa.org/#/loopy][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).
#+begin_src emacs-lisp (use-package loopy)
;; Optional support for destructuring with Dash. (use-package loopy-dash :after (loopy) :demand t) #+end_src
To load all of the alternative destructuring libraries (see section [[*Multiple Kinds of Destructuring][Multiple Kinds of Destructuring]]) and the alternative macro form (see section [[*Loop Commands in Arbitrary Code][Loop Commands in Arbitrary Code]]), use
#+begin_src emacs-lisp (use-package loopy :config (require 'loopy-iter) (require 'loopy-pcase) (require 'loopy-seq))
(use-package loopy-dash :after (loopy) :demand t) #+end_src
- Multiple Kinds of Destructuring
The default destructuring system is a super-set of what =cl-lib= provides and is described in the section [[https://github.com/okamsn/loopy/blob/master/doc/loopy-doc.org#basic-destructuring][Basic Destructuring]] in the documentation.
In addition to the built-in destructuring style, ~loopy~ can optionally use destructuring provided by ~pcase-let~, ~seq-let~, 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 [[https://github.com/okamsn/loopy/blob/master/doc/loopy-doc.org#using-flags][Using Flags]] in the documentation.
Here are a few examples that demonstrate how ~loopy~ can use destructuring with accumulation commands.
#+begin_src emacs-lisp (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)) #+end_src
For more on how =dash= does destructuring, see their documentation on the [[https://github.com/magnars/dash.el#-let-varlist-rest-body][-let]] expression.
- 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.
#+begin_src 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)) #+end_src
For more on this, [[https://github.com/okamsn/loopy/blob/master/doc/loopy-doc.org#the-loopy-iter-macro][see the documentation]].
- Adding Custom Commands :PROPERTIES: :CUSTOM_ID: adding-custom-commands :END:
It is easy to create custom commands for Loopy. To see how, see the section [[https://github.com/okamsn/loopy/blob/master/doc/loopy-doc.org#custom-commands][Custom Commands]] in the documentation.
- Comparing to =cl-loop= :PROPERTIES: :CUSTOM_ID: how-does-it-compare-to-other-approaches :END:
See the documentation page [[https://github.com/okamsn/loopy/blob/master/doc/loopy-doc.org#comparing-to-cl-loop][Comparing to ~cl-loop~]]. See also the wiki page [[https://github.com/okamsn/loopy/wiki/speed-comparisons][Speed Comparisons]].
- Real-World Examples
See the wiki page [[https://github.com/okamsn/loopy/wiki/Examples][Examples]].