Skip to content

Versus `cl loop`

okamsn edited this page Aug 21, 2021 · 13 revisions

NOTE: As Loopy develops, these examples and results might become out of date. For the most up-to-date information, refer to this package’s documentation, which is installed with the package as the Info file loopy.


Table of Contents

Why loopy?

loopy should be comparable with cl-loop for most things, keeping in mind the following:

  • It has more flexible control-flow commands, under which you can easily group sub-commands, including assignments.
  • It has a skip command to skip the rest of the loop body and immediately start the next iteration. Of course, a similar effect could be achieved using the when or unless commands.

loopy is not always one-to-one replacement for cl-loop, but it is easy to use and extend, and performs well in the cases that it already handles.

Below is a simple example of loopy vs cl-loop.

(require 'cl-lib)
(cl-loop with some-thing = 5
         for i from 1 to 100
         do (message "I is %s" i)
         when (> (+ i 5) 20)
         return (format "Done: %d" i))

(require 'loopy)
(loopy (with (some-thing 5))
       (list i (number-sequence 1 100))
       (do (message "I is %s" i))
       (when (> (+ i 5) 20)
         (return (format "Done: %d" i))))

The main benefit (I believe) of Loopy is clearer grouping of commands under conditionals while still using a clean syntax, such as in the below example.

;; => '((2 4) (4 8) (6 12) (8 16) (10 20))
(loopy (list i (number-sequence 1 10))
       (when (cl-evenp i)
         (expr once i)
         (expr twice (* 2 i))
         (collect together (list once twice)))
       (finally-return together))

;; Though you are more likely to write this as:
(loopy (list i (number-sequence 1 10))
       (when (cl-evenp i) (collect (list i (* 2 i)))))

In my experience, cl-loop does not allow the easy grouping of assignment statements under a when condition. For example, below is something I would like to try to do with cl-loop.

I am aware that in this example the for statements aren’t necessary and that the collect statements would be sufficient, but (when I come across things like this in my work) I would like to use them to declare variables for readability purposes.

(require 'cl-lib)
(save-match-data
  (cl-loop with pattern = "^Line\\([[:digit:]]\\)-Data\\([[:digit:]]\\)"
           for line in (split-string "Line1-Data1\nBad\nLine2-Data2")
           when (string-match pattern line)
           for line-num = (concat "L" (match-string 1 line))
           and for data-num = (concat "D" (match-string 2 line))

           ;; … Further processing now that data is named …

           and collect line-num into line-nums
           and collect data-num into data-nums
           finally return (list line-nums data-nums)))

;; Normal Elisp:
(save-match-data
  (let ((pattern "^Line\\([[:digit:]]\\)-Data\\([[:digit:]]\\)")
        (line-nums)
        (data-nums))
    (dolist (line (split-string "Line1-Data1\nBad\nLine2-Data2"))
      (when (string-match pattern line)
        (let ((line-num (concat "L" (match-string 1 line)))
              (datum-num (concat "D" (match-string 2 line))))

          ;; … Further processing now that data is named …

          (push line-num line-nums)
          (push datum-num data-nums))))
    (list (nreverse line-nums) (nreverse data-nums))))

Here is how one could currently do it with loopy:

(require 'loopy)
(save-match-data
  ;; The flag `split' tells `loopy' to accumulate into separate variables when
  ;; no variable is given in an accumulation command (instead accumulating into
  ;; the variable `loopy-result').  This allows for a more efficient code
  ;; expansion, since `loopy' knows that the variable won't be accessed before
  ;; the loop ends.
  (loopy (flag split)
         (with (pattern "^Line\\([[:digit:]]\\)-Data\\([[:digit:]]\\)"))
         ((list line (split-string "Line1-Data1\nBad\nLine2-Data2"))
          (when (string-match pattern line)
            (expr line-num (concat "L" (match-string 1 line)))
            (expr datum-num (concat "D" (match-string 2 line)))

            ;; … Further processing now that data is named …

            ;; If we didn't wish to use the `split' flag, then we would need to
            ;; specify variables into which to accumulate, such as in
            ;; (collect line-nums line-num)
            ;; and
            ;; (collect datum-nums datum-num)
            (collect line-num)
            (collect datum-num)))))

I believe that the value of the macro increases for longer loop bodies with several conditional commands.

Another nice ability, one that I’m not sure cl-loop has, is a specific command for skipping/continuing a loop iteration. Of course, one could also re-organize code under a conditional command like when to achieve the same effect.

;; Returns even numbers that aren't multiples of 10.
;; => (2 4 6 8 12 14 16 18)
(loopy (list i (number-sequence 1 20))
       (when (zerop (mod i 10)) (skip))
       (when (cl-evenp i)       (collect i)))

Translating from cl-loop

See the section Translating ~cl-loop~ in the Org documentation. This section is included in Loopy’s documentation when installed.