loopy icon indicating copy to clipboard operation
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]].

Local Variables:

org-link-file-path-type: relative

org-adapt-indentation: nil

End: