The Simple-yet-Powerful Loop Result Protocol

Loops return null if-and-only-if they break

One very good reason this rule exists is to make it easy to build custom loop constructs out of several other loop constructs. You can tell "from the outside" whether one of your sub-loops was BREAK'd...this way the the higher level construct is aware it shouldn't run any more of its component loop phases.

(If this rule did not exist, then implementing a loop out of several other loops would have to invasively hook and rebind BREAK in each loop body to its own, and handle that. Even if it were possible--which it probably should be--this would be complex and inefficient. So the simpler rule is better!)

To distinguish this case from normal loop results, a NULL loop body evaluation will be turned into a "boxed" NULL, e.g. a null isotope in a parameter pack:

>> for-each x [1 2 3] [null]
; first in pack of length 1
== ~null~  ; anti

>> for-each x [1 2 3] [null]
== \~(~null~)~\  ; antiform (pack!) "heavy null"

Parameter packs containing NULL will "decay" to a normal NULL when assigned to a (non-^META) variable.

...many common loops return VOID! if the body never ran

>> repeat 0 [<unreturned>]
== \~,~\  ; antiform (ghost!) "void"

>> for-each x [] [<unreturned>]
== \~,~\  ; antiform (ghost!) "void"

If the body ever runs, but evaluates to nothing, it's an empty pack (heavy void)

>> for-each x [1] [comment "hi"]
== \~()~\  ; antiform (pack!) "heavy void"

Note that some loops do not fit this pattern...e.g. an empty MAP-EACH gives an empty block:

>> map-each x [] [print "never runs"]
== []

Reacting to BREAKs (or Never Run'd) Is Easy

Loop aggregators aren't the only place that benefits from being able to tell what happened with a loop from its result. Plain user code reaps the benefits as well.

If you want to know if a loop BREAK'd or didn't run, then ELSE is the ticket:

for-each x block [
   if some-test x [break]
] else [
    ; This code runs only if the loop breaks
    ; ...so it still runs even if block is empty
]

You can combine that with THEN to segregate code when the loop doesn't break:

for-each x block [
   if some-test x [break]
] then [
    ; This code runs only if the loop doesn't break
] else [
    ; This code runs only if the loop breaks
]

Practical example?

Here's a very cool real world case from the console code:

pos: molded: mold:limit v 2048
repeat 20 [
    pos: next of any [find pos newline, break]
] then [
    insert clear pos "..."
]

You have up to 2048 characters of data coming back from the mold, ok. Now you want just the first 20 lines of that. If truncation is necessary, put an ellipsis on the end.

FIND will return NULL if it can't find the thing you asked it, so the ANY will run the break when you can't get the position. If it makes it up to 20 without breaking, the THEN clause runs.

So there you go. The first 20 lines of the first 2048 characters of a mold, truncating with "..." I think the THEN really follows the idea of completion, it makes sense (to me) that a BREAK would bypass a THEN (or an ALSO, which is similar) clause.

I encourage people to get creative, to look at ways to use this to write clearer/shorter/better code.

1 Like

I really like it. I'm curious to see what others have to say about it. I'm not the most clever ren-c coder, but I definitely see some of these new constructs enabling me to sidestep some of the bulkier expressions I often write.

1 Like

I must admit I felt a bit uneasy about "barification", so I like the new turn [of putting null in a pack, and having THEN consider a packed null a branch trigger].

1 Like

What Do Rebol2 and Red Do Here?

Rebol2 used NONE:

rebol2>> none? while [false] []
== true

But Red is using UNSET! when a loop doesn't run its body:

red>> unset? while [false] []
== true

This GitHub ticket has an inventory of Red's compiled and interpreted behaviors.

Boris says:

I haven't found any explanation of why Rebol chose that loop 0 [1] (and other non-evaluated loops) returns none rather than unset . I have however found numerous examples of relying on loops returning last result of their body evaluation. And that makes none more helpful than unset as we can chain loops into if s like unless result: loop n [stuff] [handle empty case] .

Gregg says:

Agreed on returning none consistently where the body is not processed.


Note: Their GitHub issue is from 2016, and still open in 2024...