Demonstrating FOR-BOTH: Loop Composability For The Win โ—

FOR-BOTH was an early talking point for an extremely simple usermode loop construct that would be built out of two FOR-EACH loops:

 >> for-both 'x [1 2] [3 4] [print [x], x = 5]
 1
 2
 3
 4
 ; first in pack of 1 item
 == ~null~  ; anti

A naive implementation of this in Rebol2 might look like:

 for-both-naive: func [var blk1 blk2 body] [
     foreach (var) blk1 body
     foreach (var) blk2 body
 ]

...but...

  • It will not honor BREAK correctly

    >> for-both-naive 'x [1 2] [3 4] [if x = 2 [break], print [x], x = 5]
    1
    3  ; the BREAK only broke the first FOREACH
    4
    == #[none]
    

    There's no way from the outside of Rebol2 or Red's FOREACH to know for sure that a BREAK was requested. BREAK returns NONE!, but a loop body can (and often does) evaluate to NONE! as well. Red made it even worse by adding BREAK/RETURN--so a breaking loop can return anything.

    So you'd need some kind of complex binding to search the loop bodies and bind the BREAK word to something that throws and gets caught...even for this simple goal.

  • The loop won't evaluate to the last result of the body.

    >> for-both-naive 'x [1 2] [] [print [x], x * 10]
    1  ; evaluated to 10
    2  ; evaluated to 20
    == #[none!]
    

    If the second series is empty, the fallout from the first loop is forgotten.

Behold Ren-C's Elegant Solution to FOR-BOTH

Underneath its apparent simplicity lies quite a lot of deep thought. And the mechanisms it uses apply far beyond just loops!

    for-both: func [var blk1 blk2 body] [
        return unmeta/lite all [
            meta/lite for-each var blk1 body
            meta/lite for-each var blk2 body
        ]
    ]

It solves the BREAK case

Below we see a situation where the first FOR-EACH returns NULL (and meta null is just null). So it short-circuits the ALL, and propagates the null as a signal that it broke:

>> for-both 'x [1 2] [3 4] [if x = '2 [break], print [x], x = 5]
1
== ~null~  ; anti

Note that the first pass through the loop did not terminate the ALL, just because the body evaluated to null. That's because it produced a "heavy null", and META/LITE of non-pure-void, non-pure-null antiforms produces QUASIFORM!, which is a branch trigger even if the antiform thing would not be:

 >> metanull: meta ~[~null~]~
 == ~[~null~]~

 >> type of metanull
 == &[quasiform]

>> if metafalse [print "All QUASIFORM! are branch triggers!"]
All QUASIFORM! are branch triggers!

This means the loop can gracefully recover the QUASIFORM! as the ALL result if the loop completes, and remove the quasi level:

>> for-both 'x [1 2] [3 4] [print [x], x = 5]
1
2
3
4
; first in pack of 1 item
== ~null~  ; anti

It Solves the Fallout From The Last Loop Body

This takes advantage of a new invariant: loops which never run their bodies return void.

>> for-each x [] [fail "This body never runs"]
== ~void~  ; anti

Voids act invisibly in constructs like ALL. So we get the result we want:

>> for-both 'x [1 2] [] [print [x], x * 10]
1  ; evaluated to 10
2  ; evaluated to 20
== 20

There's a slight fib here, that META/LITE of the void did not produce a "meta-void" (')...but passed it through (it would pass through NULL, as well). That's a property of the /LITE refinement:

>> meta/lite if 10 > 20 ["META/LITE passes through the void antiforms"]
== ~void~  ; anti

>> meta if 10 > 20 ["Without /LITE is more exact, gives the quasiform"]
== ~void~

But in situations like this, passing through the void state is what we wanted.

You Can Even Return NULL From the Body!

Thanks to isotopes, the following is possible:

>> x: for-both 'x [1 2] [] [print [x], if x = 2 [null]]
1
2
; first in pack of length 1
== ~null~  ; anti

>> x
== ~null~ 

How cool is that? Even though NULL is being reserved as the unique signal for loops breaking, there's a backchannel for it to escape...out of the FOR-EACH, and up out of the FOR-BOTH wrapping it!

It Holds Up Under Scrutiny!

I'm really pleased with it, and here are some tests:

ren-c/tests/loops/examples/for-both.loops.test.reb at master ยท metaeducation/ren-c ยท GitHub

I invite you to test it some more...ask questions...and perhaps come up with your own loop compositions!

:atom_symbol:

4 Likes

Pursuant to the new rules for having a return from FUNC[TION], this either needed to RETURN the result or be changed to a lambda.

I chose to add a RETURN just so that it didn't look too "foreign". (Or at least so that this talking-point example can focus its foreignness on the explanation of META and UNMETA).

But it could be written without a RETURN as:

for-both: lambda [var blk1 blk2 body] [
    unmeta/lite all [
        meta/lite for-each var blk1 body
        meta/lite for-each var blk2 body
    ]
]
1 Like