Why do ANY and ALL ignore VOID, when IF errors on it?

The evolution of "invisibility" in Ren-C gave rise to two "vaporizing intents" the VOID and NIHIL antiforms.

The default evaluator (and UPARSE) accept that void is an "opt-out state" that can't be put in blocks. But they don't vaporize VOID on a whim: it will still be the result of expressions whose last result is a void.

>> 1 + 2 if false [<a>]
== ~void~  ; anti

On the other hand, the empty parameter pack of NIHIL truly vaporizes unless you pipe it around with ^META:

>> 1 + 2 nihil
== 3

This means it's up to constructs to decide if they want to erase voids or not. DELIMIT does, so you will see that reflected in things like UNSPACED:

>> unspaced ["A" if false ["B"] "C"]
== "AC"

COMPOSE vaporizes void slots (and errors on null ones). REDUCE is currently vaporizing VOID because it seems like the default people want.

But What Should ANY and ALL Do?

We can consider that VOID is neither truthy nor falsey, and IF will reject it:

>> if (if false [true]) [<unreachable>]
** Error: IF doesn't accept VOID as its condition argument

When there was no VOID/NIHIL distinction, then ANY and ALL were backed into a corner. If they decided to error on void, you couldn't use an ELIDE or ASSERT in the middle of them. If they didn't error on VOID then the risk was that you could write something like all [1 = 1, 2 = 2, value] and if VALUE was just incidentally void you'd get the 2 = 2 result, which may not have been your intent.

Today, things like ELIDE and ASSERT return NIHIL and can be safely vaporized by ANY and ALL. This would seem to open up the choice to be consistent with IF, in erroring on voids as being neither true nor false.

One might suggest that if you really want to erase voids...you'd have something that converted voids to nihil called ELIDE-IF-VOID

>> value: void
== ~void~  ; anti

>> all [1 = 1, 2 = 2, value]
** Error: VALUE is VOID which is neither truthy nor falsey

>> all [1 = 1, 2 = 2, elide-if-void value]
== ~true~  ; isotope

Things Like FOR-BOTH Would Get More Awkward...

I was fairly proud of this formulation:

for-both: func ['var blk1 blk2 body] [
    return unmeta* all [
        meta* for-each (var) blk1 body
        meta* for-each (var) blk2 body
    ]
]

(I wound up deciding that META would meta-raise everything--including pure null and void--so the asterisks were probably best included there for the alternative formulation.)

But if ALL and ANY errored on void, and you had to erase it, this would become:

for-both: func ['var blk1 blk2 body] [
    return unmeta* all [
        elide-if-void meta* for-each (var) blk1 body
        elide-if-void meta* for-each (var) blk2 body
    ]
]

If I put on my formalism hat, I can see how this is safer. But I've made similar arguments about why I don't want this:

>> compose [a (if false ['b]) c]
** Error: COMPOSE cannot erase VOID (use ELIDE-IF-VOID if intentional)

>> compose [a (elide-if-void if false ['b]) c]
== [a c]

It is admittedly a bit different in ANY and ALL's case, because they're mixing in a test for truthiness. But erasing voids by default has other advantages in writing wild control constructs.

Erroring When You Don't Have To Inhibits Creativity

When all things are mostly equal, safety isn't as compelling as enabling creativity in my current view of the language.

I haven't seen any bad consequences of vaporizing the voids. Yet the conveniences have been proven.

So that's the decision that has been made. If you want to make a version of ANY and ALL that error on voids, you can do so with a /PREDICATE.

2 Likes