I was pondering the edge case of getting two voids into the FOR-PARALLEL example, and what it would return given the particular choice of how it was implemented:
That meant it would return whatever WHILE returned when it never ran its body, which was VOID. Currently that is distinct from what WHILE returns when its condition is void...which is NULL, according to VOID-in-NULL-out.
But is that right? Let's revisit the question of what a void input should do for loops:
>> condition: null
>> for-each item (if condition [block]) [...]
== ???
Whether we return NULL or VOID here affects things like ANY or ALL... NULL will count as a branch inhibitor and stop either construct. On the other hand, ELSE considers both pure NULL and pure VOID to be cause to run.
You could make the case this is not a void-in-null-out situation. That was invented for functions that were looking for a particular answer and couldn't give it, so instead give a "soft failure". e.g. LENGTH OF VOID is NULL, because it couldn't get the length. But loops aren't asking any particular question that fails. They're just a conduit to someone else who might be asking a question. It's kind of like how eval [void]
doesn't give you back NULL just because you evaluated and got void.
Loop result protocol says we reserve pure NULL for saying the loop encountered a BREAK. Why would we want to conflate opting out of the loop with breaking, here? That could be thought of as when it's useful to think "the question" of the loop actually did encounter a soft failure.
What About "Weird" Loops?
Asking what MAP-EACH should do is a curious one. If a MAP-EACH never runs its body, it gives back an empty block... not void as other constructs do:
>> for-each x [] [fail "this code should never run"]
== ~void~ ; anti
>> map-each x [] [fail "this code should never run"]
== []
Given current thoughts on the semantics of BLANK!, I'm thinking that should give back empty block as well... because an empty string or binary or array all give back empty block. So it doesn't break any rules for blank to do the same:
>> map-each x _ [fail "this code should never run"]
== []
In order to reason about this, we have to think about the applications.
If someone is writing code like:
all [
...
map-each x (whatever) [...]
...
]
We can imagine them having the wish to telegraph "opt out" vs. "null" to the ALL. We know that BREAK in the body of the loop telegraphs null--and that is by design. But should you be able to telegraph NULL from the (whatever)
slot, or only telegraph VOID?
Let's make it a little more concrete by saying they're using an IF in the slot:
all [
...
map-each x (if condition [whatever]) [...]
...
]
Transformation-wise, they could have written that as:
all [
...
if condition [
map-each x (whatever) [...]
]
...
]
So getting void from a conditional expression is easy in such a case. Getting NULL is trickier.
The other likely instance is a calculated expression that may be null.
all [
...
map-each x (maybe some long expression) [...]
...
]
You'd have to create a variable in order to pull that out, and not repeat the expression. Can we say anything about how likely it is that someone wants to telegraph NULL vs. VOID out of that?
And furthermore: because MAP-EACH is intrinsically a value-synthesizing construct, is this different from the likely needs of FOR-EACH or similar? Does "I made a block, or if I couldn't NULL" make more sense than "I made a block, or if I couldn't VOID"?
I'll add that the existence of MAYBE being able to turn NULL to void offers a good way to turn "I couldn't do it" to "I don't care"
all [
...
maybe map-each x (maybe some long expression) [...]
...
]
Perhaps VOID-in-NULL-out should be heeded for loops, too
It conflates void input with BREAK. But since BLANK! acts the same way as empty series, you have that other choice if you want to generate a VOID out of something like a FOR-EACH.
I think I'm going to stick with NULL as the answer for void input, for now. I'll keep an eye out for how well it is working.