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
>> meta for-each x [1 2 3] [null]
== ~[~null~]~ ; anti
Parameter packs containing NULL cannot be stored in variables, and will "decay" to a normal NULL when assigned to a variable.
...many common loops return void if the body never ran
>> repeat 0 [<unreturned>]
== ~void~ ; anti
>> for-each x [] [<unreturned>]
== ~void~ ; anti
This is also a unique result...you get void in a pack if the loop runs a body that evaluates to void:
>> repeat 1 [comment "hi"]
; first in pack of length 1
== ~void~ ; anti
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 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.
Right off the bat, if your loop body always returns a truthy thing, you can leverage the result to make sure at least one body ran and there wasn't a break:
all [
for-each x block [
if some-test x [break]
<truthy-result>
]
; ^-- falsey to interrupt ALL if block is empty, or BREAKs
...
]
If you're only concerned with whether a loop BREAKs, then ELSE is the ticket:
for-each x block [
if some-test x [break]
<truthy-result>
] else [
; This code runs only if the loop breaks
; ...so it still runs even if block is []
]
You can combine that with THEN to segregate code when the loop doesn't break:
for-each x block [
if some-test x [break]
<truthy-result>
] 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 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.
repeat 20
obviously will always try and run the body at least once. (So the loop will never return pure void here, only if you said repeat 0
)
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.