2021 UPDATE: This post uses some very outdated language from thinking circa 2017, where "void" was what we would call NULL today. But the modern behavior of ~void~ isotopes and invisibility are much more relevant, and bring the sought-after power into practice. See the explanation in this other thread...
There were several edge cases in ALL and ANY, in historical Rebol. I was reminded of the importance of these edge cases today when I noticed how Red treats commented-out code:
red> if any [false comment [foo] false] [print "Sigh."]
Sigh.
The comment evaluated to an "unset!", which in Red's world is considered to be "truthy":
red> if () [print "Red treats unsets as true? :-/"]
Red treats unsets as true? :-/
So you can't throw a comment in and expect ANY to pretend it's not there. But if you changed "unset!" to be "falsey" you'd end up with the converse problem... an ALL would fail when it shouldn't.
red> if all [true comment [foo] true] [print "Sigh if unset were falsey"]
;-- no output, in today's world
R3-Alpha correctly--I believe--considered "unset!" to be neither true nor false. It was illegal to appear in the conditional slot of an IF (or EITHER, or CASE, etc):
r3-alpha> if () [print "Won't get here."]
** Script error: if does not allow unset! for its condition argument
But this did inspire debate on what its behavior inside an ANY or ALL should be, as this status.important issue from 2009 says. Should it be an error, or ignored?
The answer was that it was ignored, mostly, unless it was the very last thing.
Gregg thought it was bad for them to be able to return an UNSET!, presumably due to the difficulty of using them in conditionals. BrianH said he had "always been surprised that it doesn’t throw an error, as that is the main difference between the unset! and none! types." At first, I agreed with both of these statements...but in time I reversed my opinion.
I now believe that it is best when voids opt-out of a "vote" in an ANY and an ALL, and that the only way to get a void result from ANY or ALL is if there were no votes.
There is a slight increased cost on ALL for this...because making all [1 + 1 ()]
return 2
requires holding on to the just-evaluated result, in case a subsequent evaluation fails.
(As a thought-experiment I've actually been slightly obsessed with figuring out a way to avoid this cost by initializing the result cell to a bit pattern and then making voids never overwrite that bit pattern. But it's fairly negligible in the scheme of things.)
The result of this decision gives you some surprisingly clever compositional power, especially now that void results are the lingua-franca of conditionals saying "no branches ran". And being able to return a void when there are no votes is a feature, not a bug. Consider:
condition1: condition2: condition3: false
number3: 3
if all [
if condition1 ["A"]
case [condition2 ["B"] condition3 ["C"]]
switch number3 [1 ["D"] 2 ["E"]]
][
print "Should you run this code or not?"
]
It seems bad to run the code, since no conditions were true and no votes occurred. So returning void leverages the stopping power of it in a conditional context. Triggering an error for IF is the right thing to do, and easy enough to fix if you meant to do that by throwing true on the end of the list. You got a potentially useful warning.
There's also the chaining power of it.
any [a b all [c d any [e f] g h] i j]
The curious property you get by not forcing the result to something truthy or falsey is that the opt-out property can propagate. This is why in Ren-C, any []
and all []
are void. The base case is "I don't know enough to tell you anything".
How useful this all is depends on whether you use it. But there's reasoning behind it, which took a long time to decide on--for better or worse!