Should ANY and ALL Vaporize VOID (or require ELIDE-IF-VOID ?)

I've written up how the evolution of invisibility gave rise to VOID vs. NIHIL.

The default evaluator (and UPARSE) accept that void is "nothing" and 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

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.

Should ANY and ALL Vaporize Voids?

To me, this is a bit of a tough decision.

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 as ~true~.

Today, things like ELIDE and ASSERT return NIHIL and can be safely vaporized by ANY and ALL, while retaining the choice to error on voids as not being true or false. If you really want to erase voids, we can have something that converts voids to nihil called ELIDE-IF-VOID

>> value: void

>> 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.

I'm really torn on this, as it's right on the edge. But when all things are mostly equal, safety isn't as compelling as enabling creativity in my current view of the language. I think I'm going to wait until I see a really bad consequence of vaporizing the voids here... it hasn't broken any real code yet, and it has proven its convenience.

2 Likes