Arc Forumnew | comments | leaders | submitlogin
Named loop or continue keyword?
2 points by garply 5245 days ago | 14 comments
Scheme has named loops, clojure has recur, and many other languages have continue for their loops, do any of you know how I might go about implementing an similar type of loop in Arc?

ccc seems like it would offer some hope, but I'm not experienced enough with continuations to be able to implement such a thing thus far. Any help?



5 points by garply 5245 days ago | link

How about something like this?

  (mac xwhile (test . body) 
    `(while ,test (ccc (fn (continue) ,@body))))
Then:

  (let x 0 
    (xwhile (< x 10) 
      (when odd.x 
        (++ x) 
        (continue)) 
      (prn x) 
      (++ x)))
does what I want it to do.

P.S. How do I format this with proper indentation on this forum?

Edit: Thanks waterhouse

-----

5 points by fallintothis 5244 days ago | link

Looks good. That's what I would've done, if you hadn't beaten me to it. :)

Relevant arc.arc definitions:

  (mac point (name . body)
    (w/uniq (g p)
      `(ccc (fn (,g)
              (let ,name (fn ((o ,p)) (,g ,p))
                ,@body)))))

  (mac catch body
    `(point throw ,@body))
Thus,

  (catch
    (while t
      (throw 5)))
expands into

  (point throw
    (while t
      (throw 5)))
which expands (modulo gensyms) into

  (ccc (fn (current-continuation)
         (let throw (fn ((o return-val)) (current-continuation return-val))
           (while t
             (throw 5)))))
So throw serves to "point back" to the current continuation, and optionally continues with a return value. (This seems to be the behavior of a simple (ccc (throw) ...) without the let; I'm not sure why it's defined this way.) The above loop returns 5 -- the continuation captured by ccc is outside of the loop, so its action is to return whatever value we say the while expression returns and continue evaluating anything after it.

This way, Arc gives you a decorator for return values. You can use this pattern to decorate any portion of your loop, to give different effects. Outside of the loop, it's like a break. Inside of the loop, it's like a continue due to how while is expanded.

  arc> (load "macdebug.arc") ; see http://www.arclanguage.org/item?id=11806
  nil
  arc> (macsteps '(while test
                    (point continue body1 body2 body3))
                 'show 'while 'point)
  Showing only: while, point

  Expression:

    (while test
      (point continue body1 body2 body3))

  Macro Expansion:

      (while test
        (point continue body1 body2 body3))

  ==> ((rfn gs2027 (gs2028)
         (when gs2028
           (point continue body1 body2 body3)
           (gs2027 test)))
       test)

  Expression:

    ((rfn gs2027 (gs2028)
       (when gs2028
         (point continue body1 body2 body3)
         (gs2027 test)))
     test)

  Macro Expansion:

      (point continue body1 body2 body3)

  ==> (ccc (fn (gs2029)
             (let continue (fn ((o gs2030)) (gs2029 gs2030))
               body1
               body2
               body3)))

  Expression:

    ((rfn gs2027 (gs2028)
       (when gs2028
         (ccc (fn (gs2029)
                (let continue (fn ((o gs2030)) (gs2029 gs2030))
                  body1
                  body2
                  body3)))
         (gs2027 test)))
     test)

  nil
As you can see, the continuation is captured around the body, but the test properly resides outside of the body, so it'll still be executed even if you continue (because, again, the default continuation is to keep on chuggin' along).

You could have a while macro encapsulate these (similar to catch) so you don't have to write

  (point break
    (while test
      (point continue
        body)))
I've been trying to think of good names for such a loop. The most descriptive words are long (e.g., continuable-while, breakable-while), but we usually recognize "esc" as "escape", so it's a short yet descriptive option. Something like

  (mac esc-while (test . body)
    `(point break (while ,test (point continue ,@body))))
or, crafting a portmanteau à la whilet,

  (mac whilesc (test . body)
    `(point break (while ,test (point continue ,@body))))
Don't know if this actually made continuations any clearer, but there you go.

-----

3 points by waterhouse 5245 days ago | link

To make code show up here properly, put two spaces before each line of code and paste it here. As in:

  ;I used this function to add spaces so I could put it here!
  (def add-spaces (xt) 
    (no:map [prn "  " _] lines.xt))
This assumes you have it indented already. If you don't, find a text editor that indents Lisp code for you (I recommend DrRacket). The 'ppr function works reasonably well, too.

-----

3 points by zhtw 5242 days ago | link

ccc is certainly way to go, but just in case: mzscheme does not use continuation passing style so the ccc can work much much slower than just a function call because it copies the stack.

Actually I didn't do the profiling so maybe there is nothing to worry about.

-----

3 points by fallintothis 5241 days ago | link

  arc> ; with ccc
  (time
    (with (x 0 y 0)
      (while (< x 1000000)
        (catch
          (when (odd x)
            (++ x)
            (throw))
          (++ y)
          (++ x)))
      (list x y)))
  time: 27208 msec.
  (1000000 500000)

  arc> ; without ccc
  (time
    (with (x 0 y 0)
      (while (< x 1000000)
        (if (odd x)
            (++ x)
            (do (++ y)
                (++ x))))
      (list x y)))
  time: 1978 msec.
  (1000000 500000)
Interesting observation!

-----

2 points by akkartik 5242 days ago | link

Ah, that helps understand ccc. I've been mulling ccc for months without having a handle on it. But seeing a simple solution to a simple problem I can relate to helps immeasurably.

-----

1 point by evanrmurphy 5244 days ago | link

Much more elegant and less buggy than my attempt. Nice work.

-----

2 points by evanrmurphy 5245 days ago | link

One way to do it would be with macro-generating macros. Take loop for example. arc.arc defines loop as follows:

  (mac loop (start test update . body)
    (w/uniq (gfn gparm)
      `(do ,start
           ((rfn ,gfn (,gparm) 
              (if ,gparm
                  (do ,@body ,update (,gfn ,test))))
            ,test))))
Here's how you could write aloop, which is just like loop except that it grants access to itself through the generated macro continue:

  (mac aloop (start test update . body)
    (w/uniq (gfn gparm)
      (mac continue ()
        `(do ,update (,gfn ,test)))
      `(do ,start
           ((rfn ,gfn (,gparm) 
              (if ,gparm
                  (do ,@body (continue))))
            ,test))))
Let's go ahead and define afor because it will be less awkward to demo with:

   ; s/loop/aloop/ was the only change from for's definition
  (mac afor (v init max . body)
    (w/uniq (gi gm)
      `(with (,v nil ,gi ,init ,gm (+ ,max 1))
         (aloop (assign ,v ,gi) (< ,v ,gm) (assign ,v (+ ,v 1))
           ,@body))))
And now check it out!

  ; prints i from zero to ten, skipping odd values of i
  arc> (afor i 0 10
         (if (odd i)
              (continue)
             (pr i)))
  *** redefining continue
  0246810nil
(Note that since the continue macro gets generated anew on each call to aloop, you'll get the "redefining continue" message every time you call aloop or afor.) You could use the above definitions to spin off labeled versions of down, forlen, each and on, and a similar technique could be applied to redefine the while-family of macros.

-----

2 points by rocketnia 5245 days ago | link

Defining macros during macro-expansion strikes me as being a bit error-prone, and here's one concern:

  (afor i 0 10
    (afor j 0 10
      ...
      (continue)
      ...
      )
    ...
    (continue)
    ...
    )
I'm pretty sure the second (continue) form is expanded after the inner (afor ...) form is expanded, so that it tries to continue the wrong loop, but ends up referring to an unbound variable instead.

I recommend sticking to an anaphoric variable that has no macro wrapping it, sort of like this:

  (mac aloop (start test update . body)
    (w/uniq (gfn gparm)
      `(do ,start
           ((rfn ,gfn (,gparm) 
              (if ,gparm
                  (let continue (fn () ,update (,gfn ,test))
                    ,@body
                    (do.continue))))
            ,test))))

-----

1 point by evanrmurphy 5245 days ago | link

I'm pretty sure the second (continue) form is expanded after the inner (afor ...) form is expanded, so that it tries to continue the wrong loop, but ends up referring to an unbound variable instead.

Very good point, I hadn't tried to nest them before.

I will try your aloop definition when I get the chance.

-----

2 points by garply 5245 days ago | link

Your code works because nothing follows your continue. This does not work as I would want it to:

(afor i 0 3 (when (odd i) (continue)) (prn i)) goes to:

0

2

4

5

nil

-----

2 points by rocketnia 5245 days ago | link

It sounds like you might be pleased to hear of an Arc utility variously known as 'afnwith, 'loop, or 'xloop: http://awwx.ws/xloop0

The workings behind 'xloop are pretty simple. Just like (with (a 1 b 2) c) expands into ((fn (a b) c) 1 2), (xloop (a 1 b 2) c) expands into ((rfn next (a b) c) 1 2).

-----

1 point by garply 5245 days ago | link

Thanks, but next in an xloop doesn't really work as a substitute, right? The code after a (next) still gets executed after the recursion completes, whereas with a continue, the rest of the branch isn't executed. I really need the latter.

-----

3 points by rocketnia 5244 days ago | link

(I wrote this response before I read some of the more recent comments, so it'll end up duplicating information, but I'm posting it anyway 'cause of the example utilities.)

In that case, I typically use 'catch or 'point (which indeed use continuations). If you want a loop with the ability to either break or continue, you can put a 'point on the outside and another 'point on the inside.

Here's a half-effort to abstract this away. I don't know how useful it'll be, but it might be the start of something better, so everyone, feel free to use it for anything without credit. ^_^

  (mac w/breaks (break next loop-header . body)
    (w/uniq g-breakpoint
      `(point ,g-breakpoint
         (let ,break (fn ((o val)) (,g-breakpoint val))
           (,@loop-header
             (point ,next
               ,@body))))))
  
  (mac forever body
    `(while t ,@body))
  
  (mac bforever body
    `(w/breaks break next (forever) ,@body))
  
  (mac bfor (var start end . body)
    `(w/breaks break next (for ,var ,start ,end) ,@body))
  
  ; What a name.
  (mac beach (var coll . body)
    `(w/breaks break next (each ,var ,coll) ,@body))
  
  ; Turn any loop into a breakable one. Example usage:
  ;
  ; (b-:until (all has-come-home cows) :
  ;   (tip rand-elt.cows)
  ;   (unless whales
  ;     (next))
  ;   (actually-beach rand-elt.whales)
  ;   (when cops
  ;     (prn "Uh, oops?")
  ;     (break)))
  ;
  (mac b- (loop)
    (let (header (_ . body)) (split loop (pos ': loop))
      `(w/breaks break next ,header ,@body)))
Silly example:

  (def going-up (top-floor)
    ; Alternately, "(b-:for floor 1 top-floor :".
    (bfor floor 1 top-floor
      (when (and (is floor 13) (~is top-floor 13))
        (next))
      (when (< 20 floor)
        (prn "...And that's it.")
        (prn "Sorry, this elevator is afraid of heights.")
        (break))
      (prn "Floor " floor (case floor 13 "?? O_O" "!"))))
(Clearly, this wouldn't mix very well with 'xloop; since an 'xloop doesn't loop unless you make it loop, a "continue" continuation wouldn't really make any sense.)

-----