Parallel FOR Example

One goal of Ren-C since the start has been to make sure people can write their own looping constructs that behave like the built-in loops. (That's why definitional return was so important, which paved the way for many other improvements to come.)

One loop construct requested by @gchiu was the ability to walk through blocks in parallel. It's in the tests, but I thought I'd share it here:

for-parallel: function [
    return: [any-atom?]
    vars [block!]
    blk1 [~void~ any-list?]
    blk2 [~void~ any-list?]
    body [block!]
][
    return while [(not empty? maybe blk1) or (not empty? maybe blk2)] [
        (vars): pack [(first maybe blk1) (first maybe blk2)]

        repeat 1 body else [  ; if pure NULL it was a BREAK
            return null
        ]

        ; They either did a CONTINUE the REPEAT caught, or the body reached
        ; the end.  ELIDE the increment, so body evaluation is WHILE's result.
        ;
        elide blk1: next maybe blk1
        elide blk2: next maybe blk2
    ]
]

You get this behavior:

>> collect [
       assert [
           20 = for-parallel [x y] [a b] [1 2] [
               keep :[x y]
               y * 10
           ]
       ]
   ]
== [[a 1] [b 2]]

There's a lot of nice little flourishes here. BREAK and CONTINUE work, thanks to the loop result protocol. Assigning the variables is handled elegantly by multi-return, where a SET-GROUP! retriggers as a SET-BLOCK!. ELIDE is put to good use to avoid a temporary variable for the loop product.

"Isn't it nice... when things just... work?"

2 Likes

I noticed that in the reduced case of receiving two voids, this will return what WHILE returns if it never runs its body. That is VOID.

However, that is contentious with the VOID-in-NULL-out policy. I wrote some ponderings on what the right answer should be, and I lean towards saying that loops should obey VOID-in-NULL-out.

If that were to be heeded here, it would need something like:

    ...
    blk1 [~void~ any-list?]
    blk2 [~void~ any-list?]
    body [block!]
][
    all [void? blk1, void? blk2] then [return null] 
    return while [(not empty? maybe blk1) or (not empty? maybe blk2)] [
        ...

(There's probably a cleverer way to write it--there usually is--but that's straightforward enough.)

But I'm a little on the fence on if loops should do VOID-in-VOID-out vs. VOID-in-NULL-out, so I'm just going to start paying closer attention to which seems more useful.

(The answer might depend on the kind of loop. For instance, MAP-EACH being "expected" to produce a value should contrast it with NULL if it couldn't. Whereas FOR-EACH and FOR-PARALLEL and such might be more like EVAL and be expected to tunnel whatever evaluative result it gets.)