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]]
1
2
3
4
A naive implementation of this in Rebol2 might look like:
rebol2>> for-both-naive: func ['var blk1 blk2 body] [
foreach (var) blk1 body
foreach (var) blk2 body
]
...but...
-
It will not honor BREAK correctly
rebol2>> 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.
rebol2>> 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
Quick notes for those coming from historical Redbol:
-
LAMBDA is just FUNC that doesn't have a definitional RETURN (...so body result falls out)
-
Since
'argunbinds arguments in Ren-C (and'can modify any parameter convention),@argmeans take it literally,@(arg)means take literally but allow parentheses to escape. -
Terminal slash (paths ending in blank) means "GET but don't execute"...like a historical GET-WORD!...but also ensures it's a function. (Good semiotics since it throws up a "wall" between the word and any subsequent arguments, to indicate it's not taking those arguments.)
-
The
^operator in this context means "I know what I'm doing: allow this expression to vanish if it returns a void."
for-both: lambda [@(var) blk1 blk2 body] [
all [
^ for-each (var) blk1 body then lift/
^ for-each (var) blk2 body then lift/
] then unlift/
]
If you prefer English words, the ^ exists as a function called IDENTITY.
for-both: lambda [@(var) blk1 blk2 body] [ all [ identity for-each (var) blk1 body then lift/ identity for-each (var) blk2 body then lift/ ] then unlift/ ]I feel it's easier to "see" the code if you use
^, but if you are allergic to symbols, YMMV. (It's a part that has to be there because FOR-EACH vanishing by default if the loop never runs seems empirically unwise. Although you could disagree and change that, too.)
The strategy here is that we want to LIFT anything that a THEN would react to, leaving "light void" and "light null" in their unlifted states. This lets them serve as the signal for breaking or opting out of contributing to the final loop result:
>> lift ()
== ~
>> () then lift/
== \~\ ; antiform (void!)
>> lift null
== ~null~
>> null then lift/
== \~null~\ ; antiform (logic!)
So when the lift doesn't happen we can get null or void. FAILURE! antiforms will also not trigger the THEN, hence they will escalate to PANIC.
It solves the BREAK case
Below we see a situation where the first FOR-EACH returns NULL (and null then lift/ 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~\ ; antiform (logic!)
Note that the first pass through the loop did not terminate the ALL, just because the body evaluated to null (x = 5 is null). That's because that branch evaluation produced a "heavy null", and LIFT of non-ghost, non-light-null antiforms produces QUASIFORM!, which is a branch trigger even if the antiform thing would not be:
>> ~(~null~)~
== \~(~null~)~\ ; antiform (pack!) "heavy null"
>> lifted: lift ~(~null~)~
== ~(~null~)~
>> type of lifted
== \~{quasiform!}~\ ; antiform (datatype!)
>> if lifted [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
== \~(~null~)~\ ; antiform (pack!) "heavy null"
It Solves the Fallout From The Last Loop Body
This takes advantage of a new invariant: loops which never run their bodies return voids.
>> for-each x [] [panic "This body never runs"]
== \~\ ; antiform (void!)
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
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
== \~(~null~)~\ ; antiform (pack!) "heavy null"
>> x
== \~null~\ ; antiform
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.r 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!
![]()
UPDATE: Prefix Operator THENCE ... and LIFT* / UNLIFT*
It's hard to name a prefix operator that means "If THEN would act on the second argument, run the first argument on it as a handler."
But Gemini suggested THENCE, and I think that is about as good as it gets:
thence f ( expr ) ≡ ( expr ) then f
So we can write:
for-both: lambda [@(var) blk1 blk2 body] [
thence unlift/ all [
^ thence lift/ for-each (var) blk1 body
^ thence lift/ for-each (var) blk2 body
]
]
The existence of an elegant postfix formulation should hopefully make people more empathetic to why it's hard to name. I think putting it in the box as LIFT* and UNLIFT* is probably fine:
for-both: lambda [@(var) blk1 blk2 body] [
unlift* all [
^ lift* for-each (var) blk1 body
^ lift* for-each (var) blk2 body
]
]
I'm just glad it's not what it was at first ("LIFT:LITE" and "UNLIFT:LITE")! (All right, we're here. Let us never speak of the shortcut again...)