NonGNU ELPA - loopy

loopy Atom Feed

Description
A looping macro
Latest
loopy-0.15.0.tar (.sig), 2025-Nov-07, 1.13 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.svg loopy.svg loopy-badge.svg loopy-badge.svg


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:

  • Version 0.15.0:
    • Loopy now requires at least Emacs version 28.1, increased from version 27.1.
    • set now warns when it is not given a value. In the future, it will signal an error.
    • loopy-command-parsers and loopy-aliases are deprecated in favor of a single hash table in the new user option loopy-parsers. This simplified the code and will make adding local overrides easier.
    • loopy-iter-bare-special-macro-arguments and loopy-iter-bare-commands are deprecated in favor of the single variable loopy-iter-bare-names.
    • when and unless are now implemented separately, fixing when the commands are aliased.
    • Modifications to an implied loopy-result in finally-do will now be included in the implied return value of the macro.
    • loopy-default-flags is made obsolete to avoid conflicts between libraries. Using a wrapping macro instead, such as the one given in Changelog or the Org/Info documentation.
  • 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 supports destructuring for iteration commands like list and accumulation commands like sum or collect.

;; Summing the nth elements of sub-arrays:
;; => (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 currently provides this feature as a separate macro, loopy-iter, which expands looping constructs using macroexpand (see Loop Commands in Arbitrary Code in this README).

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))
;; 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))

;; => 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.tar.lz2025-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.

0.15.0

New Features
  • Add the override special macro argument, which can be used to override select global settings for the current macro expansion ([#243], [#231]). This can help to avoid name collisions from package authors (separate authors of separate packages) wanting to use the same command name for different purposes with their respective package.
Bug Fixes
  • when and unless now correctly work when aliased ([#234], [#240]).
  • Fix variable scoping when using set with at ([#241]).
Breaking Changes
  • Loopy now requires at least Emacs version 28.1, increased from version 27.1 ([#446]). This allows us to remove workarounds for missing features/fixes.

  • set now warns when it is not given a value ([#229]). Currently, (set VAR) binds VAR to nil, but since this form is indistinguishable from a mistake, and since nil is a short word to write, this behavior is deprecated.

  • Some variables were combined to simplify the code internally and make it easier to add local overrides in the future, which will make code which custom commands more portable.

    • loopy-command-parsers and loopy-aliases are both deprecated in favor of the newly added loopy-parsers ([#237]). The new user option is a hash table which maps symbols to parsing functions. There is no longer a separate mapping of aliases to original names. However, loopy-defalias will continue to work.

    • loopy-iter-bare-special-marco-arguments and loopy-iter-bare-commands are both deprecated in favor of the newly added loopy-iter-bare-names ([#242], [#238]). The new user option is a list which by default contains all symbols previously listed in the old variables.

  • Separate when and unless commands to have different parsing functions ([#234], [#240]). The old implementation used the name of the command in the generated code and was written before aliases.

  • The macro by default now uses the value of loopy-result as the implied return value when loopy-result is used as an implied accumulation variable and finally-do is used ([#244]). Previously, the macro would store the implied return value in loopy-result when loopy-result was used as an implied accumulation variable, but any further changes made to loopy-result after the loop completed in the finally-do special macro argument would not be included in the macro's ultimate return value.

    This was inconsistent with modifying loopy-result in the after-do special macro argument, in which case the modification /was/ included in the implied return value.

    The new behavior should be less confusing in the event that a user does modify loopy-result in finally-do. Previously, one would have needed to use finally-return after modifying an implied loopy-result. The cost is switching from the use of prog1 to an if expression with two helper variables. This change only applies when an implied return value is used with finally-do. It does not affect the macro expansion when finally-return is used.

    ;; Previously required way:
    ;; => (0 1 2 3)
    (loopy (list i '(1 2 3))
         (collect i)
         (finally-do (push 0 loopy-result))
         (finally-return loopy-result))
    
    ;; Now, `finally-return' is not required:
    ;; => (0 1 2 3)
    (loopy (list i '(1 2 3))
         (collect i)
         (finally-do (push 0 loopy-result)))
    

    This is technically a breaking change for those who were modifying an implied loopy-result but did not wish those changes to be captured in the macro's implied return value nor used in finally-return (perhaps changing the value for side effects only).

  • loopy-default-flags is now deprecated ([#245]). This prevents one library from breaking the macro expansions in another library. Instead of using ... ...