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.