How to "break" the loop?

Our task is to create something that looks like a “for” command from GoLang
We have some code that looks like this:

(defmacro for-loop [[symbol initial check-exp post-exp] & code]
 `(loop [~symbol ~initial]
    (if ~check-exp
      (do
        [email protected]
        (recur ~post-exp)))))

The question is how to do the “break” and “continue” commands since we can’t throw recurs in the middle of the code.

It is being called like this:

 (for-loop [i 0 (< i 10) (inc i)]
           (print i))

I think it kind of depends on the context of your question.

If I needed to do this at work, I’d just use doseq:

(doseq [i (range)
        :while (< i 10)]
  (print i))

If the goal is to just make a function, I’d do this (because I avoid macros whenever possible):

(defn for-loop
  [[initial check-fn post-fn] code]
  (doseq [i (iterate post-fn initial)
          :while (check-fn i)]
    (code i)))

(for-loop [0 #(< % 10) inc]
          #(print %))

If this is a puzzle for creating the exact function you describe, I’d do this:

(defmacro go-for-loop [[symbol initial check-exp post-exp] & code]
  `(let [check-fn# (fn [~symbol] ~check-exp)
         post-fn# (fn [~symbol] ~post-exp)
         code-fn# (fn [~symbol] [email protected])]
      (for-loop [~initial check-fn# post-fn#] code-fn#)))

(go-for-loop [i 0 (< i 10) (inc i)]
              (print i))
3 Likes

Thank you, but how can we implement the “break” command in Clojure?
E.g. we call our macro this way

(for-loop [i 0 (< i 10) (inc i)]
           (if (= i 5)
               (break))

Could you give a little more context here? If this is just a puzzle and you want it to be a pure function, I suspect what you’re describing could be achieved with a continuation, but it would likely be messy. If you don’t care about purity, you could use a keep-going? atom that holds a boolean outside the doseq, then check that before calling check-fn. You’d need to pass a function that sets it to false to your code (or maybe define that symbol while you’re evaluating code in the macro version). The downside of all this is that the notion of when to stop is split across two different places (check-exp and code). I’m finding it hard to say what a good way forward would be here without a little more context.

1 Like

Hello, @KACHANIX!

I generally agree with what @bmaddy is saying here. Imperative looping isn’t considered idiomatic in Clojure, functional approaches are generally preferred. As @bmaddy mentioned in his last comment, it could be useful to discuss the wider problem here – as the wider problem may have a neat functional solution.

That being said, Rosetta Code has a Clojure example that uses break in imperative languages, but loop/recur in Clojure. The following might compile to a Java loop using break:

Show a loop which prints random numbers (each number newly generated each loop) from 0 to 19 (inclusive).

If a number is 10, stop the loop after printing it, and do not generate any further numbers.

Otherwise, generate and print a second random number before restarting the loop.

If the number 10 is never generated as the first number in a loop, loop forever.

(loop [[a b & more] (repeatedly #(rand-int 20))]
  (println a)
  (when-not (= 10 a) 
    (println b) 
    (recur more)))

I hope that helps!

3 Likes
(defmacro go-for-loop [loop-definition & body]
  `(let [continue# (atom true)
         ~'break (fn [] (do 
                          (println "BREAKING!") 
                          (swap! continue# (constantly false))))]
    
    (doseq [[email protected] :while @continue#]
           [email protected])))


  (go-for-loop [x (range 0 10)] 
..   (if (>= x 5) 
..     (break) 
..     (println x)))
0
1
2
3
4
BREAKING!
=> nil

It’s not pretty :smiley: The macro uses an anaphoric binding to access a function that sets the :while atom to false. If you want to know more about anaphoric macros I recommend reading Paul Graham’s On Lisp, and Doug Hoyte’s Let Over Lambda.

1 Like

I think using exceptions is the simplest and maybe only way here. Otherwise there is no way to return from a non leaf position.

Break and continue would both throw. Your macro would try/catch it. Continue can be try/catched inside the body of the loop, and calls recur in the catch clause. Your break would try/catch outside the loop, and just return in its catch.

I second everyone else though. Apart from a fun experiment, please don’t let this make it inside of production code :yum:

2 Likes

Interesting to provide break as a bound function. I have one question about termination, though:

(defmacro go-for-loop [loop-definition & body]
  `(let [continue# (atom true)
         ~'break (fn [] (do
                          (println "BREAKING!")
                          (swap! continue# (constantly false))))]

     (doseq [[email protected] :while @continue#]
       [email protected])))

(go-for-loop [x (range 0 10)]
             (if (>= x 5)
               (do
                 (break)
                 (prn "Should this happen?"))
               (println x)))
0
1
2
3
4
BREAKING!
"Should this happen?"
;; => nil

Do you use anamorphic macros yourself when you code?

1 Like

I don’t personally use them, they’re pretty fiddly and as you can see I’m no master. Feel free to play around with the macro and improve it!

I don’t really see the big advantage in using them over binding a symbol explicitly, but I’d love to be shown otherwise.

1 Like

I will treat ‘break’ as a keyword (i.e. ::break)

(defmacro for-loop
  {:style/indent 1}
  [[symbol initial check-exp post-exp] & code]
  `(loop [~symbol ~initial]
     (when (and ~check-exp (not= ::break [email protected]))
       (recur ~post-exp))))

This will print 0 to 4

(for-loop [i 0 (< i 10) (inc i)]
  (if (= i 5)
    ::break
    (println i)))

And this will print 0 to 9 except 5

(for-loop [i 0 (< i 10) (inc i)]
  (if (= i 5)
    ::continue
    (println i)))
2 Likes

Since everyone is taking a stab at it :stuck_out_tongue:

(defmacro for-loop [[s i c p] & body]
  `(letfn [(~'break [] (throw (ex-info nil {:type :break})))
           (~'continue [] (throw (ex-info nil {:type :continue})))]
     (try
       (loop [~s ~i]
         (when ~c
           (try
             [email protected]
             (catch Exception e#
               (when-not (= :continue (:type (ex-data e#)))
                 (throw e#))))
           (recur ~p)))
       (catch Exception e#
         (when-not (= :break (:type (ex-data e#)))
           (throw e#))))))
(for-loop [i 0 (< i 10) (inc i)]
          (when (= 3 i)
            (continue))
          (when (= 5 i)
            (break))
          (println i))
0
1
2
4
nil

A fully imperative for loop! What a monster :wink:

Or if we prefer to use deref for break and continue:

(defmacro for-loop [[s i c p] & body]
  `(let [~'break (delay (throw (ex-info nil {:type :break})))
         ~'continue (delay (throw (ex-info nil {:type :continue})))]
     (try
       (loop [~s ~i]
         (when ~c
           (try
             [email protected]
             (catch Exception e#
               (when-not (= :continue (:type (ex-data e#)))
                 (throw e#))))
           (recur ~p)))
       (catch Exception e#
         (when-not (= :break (:type (ex-data e#)))
           (throw e#))))))
(for-loop [i 0 (< i 10) (inc i)]
          (when (= 3 i)
            @continue)
          (when (= 5 i)
            @break)
          (println i))
0
1
2
4
nil
3 Likes

@didibus @teodorlu @rmcv @bmaddy @mel
Thanks y’all!

1 Like