2

Does the cl-loop macro implement an equivalent to the continue keyword of other languages?

The behavior of break can be achieved by using until or while clauses by placing them in the middle of cl-loop, e.g.

(cl-loop item in '(1 2 3 4)
         do (print item)
         until (= 3 item)
         do (print item))

will be roughly equivalent to Python code

for item in [1, 2, 3, 4]:
    print(item)
    if item == 3:
        break
    print(item)

However, I cannot identify any clause, that says "abort this iteration step, continue with the next", like the continue keyword would do. Is there such a clause?

Usage example

Let's say I want to do something for every string in a list. From the string I want to derive a file-name, and if it exists, collect data about those files.

With a continue clause:

(defconst mylist '(1 2 "hello" 3 "world" 4))

(defconst mylist-file-data 
  (cl-loop for prefix in mylist
           unless (stringp prefix) continue
           for file-name = (concat prefix ".txt")
           unless (file-exists-p file-name) continue
           for attributes = (file-attributes file-name)
           for mtime = (file-attribute-modification-time attributes)
           for size = (file-attribute-size attributes)
           collect (list file-name mtime size)))

By contrast, without a continue clause I cannot use for clauses to define variables, that can only be defined when the condition is fulfilled. Instead I need to reformulate the loop more awkwardly, e.g. as

(defconst mylist-file-data
  (cl-loop for prefix in mylist
           for file-name = (if (stringp prefix) (concat prefix ".txt"))
           for attributes = (if (file-exists-p file-name) (file-attributes file-name))
           for mtime = (if attributes (file-attribute-modification-time attributes))
           for size = (if attributes (file-attribute-size attributes))
           if attributes
           collect (list file-name mtime size)))
           

Note how all (if ...)s are executed for each iteration step, even when the first already decides that the step could be skipped.

Drew
  • 75,699
  • 9
  • 109
  • 225
kdb
  • 1,561
  • 12
  • 21
  • 1
    Not an answer, but the `cl-loop` macro is an elisp implementation of Common Lisp's `loop` macro. You could read up on the latter in [this chapter from "Practical Common Lisp"](http://www.gigamonkeys.com/book/loop-for-black-belts.html) to see if the construct you want is available in some form. – Dan Mar 03 '21 at 15:17
  • @Dan No such luck. But `cl-loop` contains many Emacs specific extensions, so I may hope. – kdb Mar 03 '21 at 15:32
  • 1
    `catch/throw` are general (if low-level) mechanisms to implement alternative control structures (like `break` and `continue`). How to fit them into `cl-loop` however is beyond me: I don't understand `cl-loop`... – NickD Mar 03 '21 at 15:44
  • @NickD Not applicable in `cl-loop`. With plain elisp it would be something like `(catch 'break (while COND (catch 'continue BODY)))`. – kdb Mar 03 '21 at 17:23
  • That's not quite true: there is an implicit block defined with `cl-loop` (nil by default but it can be named if you want) and `cl-block` is a macro that sets up a `catch` with that name; then `cl-return-from` does a `throw`. That's basically a `break`. I still don't know how to do a `continue` though (not even in the `while` case: your example is wrong I think because `continue` should evaluate the `COND`). – NickD Mar 03 '21 at 20:13

4 Answers4

1

If you are looking for short-circuiting logic, e.g. nothing proceeds after one condition fails, I think you need to use something like the and macro. I don't know a way to do this other than to let bind some variables, and use setq inside the loop like below. The attributes line will only be set when the file-name exists here.

#+BEGIN_SRC emacs-lisp
(defconst mylist '(1 2 "hello" 3 "world" 4))

(let (file-name attributes)
  (cl-loop for prefix in mylist
       when (and (setq file-name (and (stringp prefix) (concat prefix ".txt")))
              (file-exists-p file-name)
              (message "getting attributes for %s" file-name)
              (setq attributes (file-attributes file-name)))
       collect
       (list file-name
         (file-attribute-modification-time attributes)
         (file-attribute-size attributes))))
#+END_SRC

It isn't quite continue like you want, but is functionally the same I think. The only benefit of this over your solution above is the short-circuiting logic though.

Building on the dolist example that @Drew suggested below, here is a version that uses when-let* which I think also short circuits itself and stops evaluating its arguments on the first nil value.

#+BEGIN_SRC emacs-lisp
  (defconst mylist '(1 2 "hello" 3 "world" 4))

  (setq result ())

  (dolist (prefix  mylist)
    (when-let* ((p0 (stringp prefix))
        (file-name   (concat prefix ".txt"))
        (p1 (file-exists-p file-name))
        (attributes  (file-attributes file-name))
        (mtime       (file-attribute-modification-time attributes))
        (size        (file-attribute-size attributes)))
      (message "fn: %s" file-name)
      (push (list file-name mtime size) result)))
  result
#+END_SRC
John Kitchin
  • 11,555
  • 1
  • 19
  • 41
  • Using `if/unless` clauses is too limited. Let's say I want to iterate over a list, and for all strings in the list perform further computations. With a `break` clause that would be `(cl-loop for item in items unless (stringp item) break for foo = (bar item) ...)`. Using `unless (stringp item)` I can only chain accumulation and `do` clauses, but cannot use any `for` clauses and have to connect the clauses with `and`. – kdb Mar 03 '21 at 12:58
  • 1
    you should provide an actual example of what you want to do. The example I showed is equivalent to using continue in a Python loop. – John Kitchin Mar 03 '21 at 13:03
  • Added an example. – kdb Mar 03 '21 at 15:16
1

This "answer" is not meant to be an the answer to the question because it is not "using cl-loop". But rather provide an alternative (as many other answers have done already).

I would recommend using loopy. According to its readme, Loopy is an (external) emacs package that provides a "a macro meant for iterating and looping [that is] similar in usage to ~cl-loop~ but uses symbolic expressions rather than keywords."

It already has a continue command (whose alias is "skip"). Here is the relevant section of it's manual:

2.2.4.2 Skipping an Iteration
.............................

‘(skip|continue)’
     Go to next loop iteration.

          ;; => (2 4 6 8 12 14 16 18)
          (loopy ((seq i (number-sequence 1 20))
                  (when (zerop (mod i 10))
                    (skip))
                  (when (cl-evenp i)
                    (push-into my-collection i)))
                 (finally-return (nreverse my-collection)))

For the specific loop you posed as an example, I think this is the equivalent as loopy loop. Pretty straightforward translation.

(loopy ((list prefix mylist)
    (unless (stringp prefix) (continue))
    (expr file-name (concat prefix ".txt"))
    (unless (file-exists-p file-name) (continue))
    (expr attributes (file-attributes file-name))
    (expr mtime (file-attribute-modification-time attributes))
    (expr size (file-attribute-size attributes))
    (collect (list file-name mtime size))))

One Caveat: loopy is still in its infancy and is currently undergoing some change in its syntax (see #33). However, it is more than usable and these changes will make it even more similar to cl-loop and easier do nested loops with.

Aquaactress
  • 1,393
  • 8
  • 11
1

As @NickD pointed out, you can also use cl-block and cl-return-from to simulate continue.

(cl-loop for item in '(1 2 3 4)         
    with items                          ;; declare variable `items'
    do (cl-block 'iteration             ;; put the iteration body in a block  
        (when (= 3 item)
          (cl-return-from 'iteration))  ;; jump out the whole iteration if item = 3
        (push item items))              
    finally return (nreverse items))    ;; return '(1 2 4)

In the same spirit, your example could be implemented like this:

(defconst mylist-file-data 
  (cl-loop for prefix in mylist
           with file-data-list
           do (cl-block 'iteration
                (let (file-name attributes mtime size)
                  (unless (stringp prefix)
                    (cl-return-from 'iteration))
                  (setq file-name (concat prefix ".txt"))
                  (unless (file-exists-p file-name)
                    (cl-return-from 'iteration))
                  (setq attributes (file-attributes file-name))
                  (setq mtime (file-attribute-modification-time attributes))
                  (setq size (file-attribute-size attributes))
                  (push (list file-name mtime size) file-data-list)))
            finally return (nreverse file-data-list)))

Or, to get rid of those setq

(defconst mylist-file-data 
  (cl-loop for prefix in mylist
           with file-data-list
           do (cl-block 'iteration
                (let* ((prefix-string? (unless (stringp prefix)
                                         (cl-return-from 'iteration)))
                      (file-name (concat prefix ".txt"))
                      (file-name-exists? (unless (file-exists-p file-name)
                                           (cl-return-from 'iteration)))
                      (attributes (file-attributes file-name))
                      (mtime (file-attribute-modification-time attributes))
                      (size (file-attribute-size attributes)))
                   (push (list file-name mtime size) file-data-list)))
           finally return (nreverse file-data-list)))

But at this point, @John Kitchin's answer using when-let* looks similar but cleaner.

Firmin Martin
  • 1,265
  • 7
  • 23
0

This doesn't answer your question about CL's loop. But it shows you another, simple way to do what you apparently want to do.

(let ((result  ())
      file-name attributes mtime size)
  (dolist (prefix  mylist)
    (unless (stringp prefix)
      (setq file-name   (concat prefix ".txt")
            attributes  (file-attributes file-name)
            mtime       (file-attribute-modification-time attributes)
            size        (file-attribute-size attributes))
      (push (list file-name mtime size)))))

And if you really do want to break out of the loop at any point (instead of continueing), just wrap the dolist in a catch, and then throw to that catch, throwing it whatever value you like.

Drew
  • 75,699
  • 9
  • 109
  • 225
  • I think if you use when-let* inside the dolist it would be closer to the short-circuiting logic the OP was looking for. – John Kitchin Mar 04 '21 at 01:25
  • @JohnKitchin: What short-circuiting? I thought he wanted to just continue, skipping the actions for that iteration and continuing with the next iteration. (And it's not clear to me how you are suggesting to use `when-let*` in the `dolist` - using it to do what?) – Drew Mar 04 '21 at 01:43
  • My interpretation was he wanted to continue after the first failed logic, e.g. if the file doesn't exist, i.e. to not also check all the following conditionals. That is I believe what happens in the and macro, where it returns nil at the first nil conditional and does not evaluate the rest of them. I added the idea with when-let* in my answer above. – John Kitchin Mar 04 '21 at 02:58