VOID! (and the end of "blankification")


Ren-C now has a VOID! datatype. It’s what PROCEDURE returns, and it’s unfriendly (on purpose):

>> type of print "Hi"
== void!

>> if (print "Hi") [print "can't use in IF or CASE conditions"]
** Script Error: VOID! values are not conditionally true or false

>> var: print "can't assign via SET-WORD! or SET-PATH!"
can't assign via SET-WORD! or SET-PATH!
** Script Error: var can't be assigned a VOID! (override with SET*)

So it may look even meaner than NULL. But unlike NULL, it is a value and you can put it in arrays:

>> data: copy [a b c]
>> append data print "will add to array"
== [a b c #[void]]

>> do compose [print "if seen by the evaluator" (print "They error")]
They error
if seen by the evaluator
** Script Error: Cannot evaluate VOID! ;-- Note: Rebol2/Red/R3-Alpha ignore UNSET!

Hence it is a lot like a historical UNSET!, except one very foundational thing:

>> set* 'var print "If you are persistent, you can set it"
If you are persistent, you can set it
== #[void]

>> unset? 'var ;-- e.g. Rebol2s `not value? 'var`
== #[false] ;-- Big difference!

>> void? var ;-- plain WORD! gets it, no GET-WORD! needed
== #[true]

So it is an ANY-VALUE!, and as such, a variable holding it IS SET.

This is nice because ever since NULL became legitimately falsey, there wasn’t protection for when the result of a procedure call got tested for logic, or composed and executed. So for instance: now your PRINTs in ANY and ALL and similar won’t silently act falsey, they’ll error…and to work around this, just say elide print … and it will run, but the result will be “invisible” and not processed.

As nice as that is, that wasn’t the motivator…

The motivation was to put an end to “blankification”

If you haven’t been working with the new ELSE+ALSO+UNLESS+OPT+TRY+!!+??+DEFAULT+MAYBE+MATCH and everything related… what’s wrong with you, this stuff is amazing. :slight_smile:

But if you had been working with the routines, you would have noticed the appearance of new-and-likely-confusing refinements. The names would vary…but you’d see if/opt, switch*, case/only. The issue was this:

ELSE was an experiment in Ren-C that began driving the idea that it was necessary to be able to tell from the outside of a conditional expression (like an IF, CASE, SWITCH) whether it took a branch or not. The concept was to use NULL as a signal for this–since it did not represent a value.

Yet since NULL was an evaluative result, it was possible to get one in the branch. This situation arose:

if true [
   ;-- code that makes a null result
] else [
   print "this shouldn't run, but would if it got null"

To work around this, the idea of “BLANK!-ification” came up. If a branch ran and returned null, it would be converted into a blank. You would lose the distinction, but nulls were not particularly common. Their closest relative was the UNSET!, and you didn’t see much code that intentionally handled them.

The blankification came to be applied to every branch…not just the IF, but the ELSE as well. In order to avoid casual errors for people flipping between EITHER and IF…ELSE, EITHER was subjected to the same rules. Even though it didn’t need the signaling that IF did, it still required a special refinement to ask to pass through the null:

>> either true [] []
== _

>> either false [] []
== _

>> either/opt true [] []
;-- null

>> either* false [] [] ;-- * specialization same as /OPT
;-- null

Users of these refinements were taking the risk that they wouldn’t be able to use things like ELSE correctly, but they accepted that.

Then things changed… NULLs arose to be a major signal–and auto-converting them to blanks proved to not be always what you wanted.

A Better Idea

The better idea switches from blankification to VOID!-ification… because VOID!s have more congruent properties with a NULL that has not been explicitly disarmed by a TRY.

This allows more safety in moving to a philosophy where if a branch doesn’t have to mutate a result for its usage pattern, then leave it alone. So while IF has to alter a NULL somehow in order to differentiate the signal that a branch was run to ELSE, there is no such rule forcing ELSE’s hand regarding its branch. And there’s no reason for EITHER to do it at all–you should even be able to use EITHER with an ELSE that tests its branch outcomes for nullity!

Under the new rules, examine these two conditionals…both of which have both branches as evaluating to NULL:

x: if condition [third [a b]] else [second [z]]
y: either condition [third [a b]] [second [z]]

Whether condition is true or false, neither will permit the assignment. IF is now producing VOID!s for its null branch–not BLANK!s.

While the EITHER can give a true NULL back in the true case, the IF gives back something spiky enough to draw attention to it. And in the falsey branches of both, the NULL can be given unhindered…since ELSE doesn’t have to disrupt any NULLs it produces.

As a technical matter, the OPT action now also considers VOID! along with BLANK! as a candidate for conversion to NULL. So it’s possible to write:

>> opt case [
   ... ["a b c"]
   ... [] ;-- no-op
   ... ["d e f"]
;-- null

…and not wind up with a VOID! value that would cause errors in response to the NO-OP branch.

What the simplifcation hinges on…

The old system was based on the idea that when push came to shove, people needed to know when something like an IF or a SWITCH returned NULL vs. BLANK!. That distinction started out a little important, and then became increasingly important. Somehow the conditional had to be told to act differently than it did by default, which subsequently ruined its interaction with things like ELSE, etc.

This new system is based on the idea that you do not now–or in the future–care about the distinction between whether a branch ran and made a VOID! vs. a NULL. In the general case you can’t know. It also assumes that if you’re going to be deliberately making a branch evaluate to a NULL, you’ll be doing it in the else.

That’s not such a bad bet. Increasingly a lot of ELSEs don’t even have IFs to their left… it’s not if x: select … […] else […] but x: select … else […]. And with this change that ELSE doesn’t taint its branch evaluations, you can even write chains of ELSE:

if condition [...] else [third [a b]] else [find [a b] 'c] else [<not-found>]

And also–importantly–since there’s no way to get an IF or a SWITCH to pass through NULL in taken branches unvoided, you can’t mess up their interactions with ELSE. So that’s another thing no one has to worry about.

One important moral of the story is: don’t try treating voids nicely. Even though they are values and variables holding them are set… you should spurn them like the freaks they are. They cause all those errors for a reason…just not a “variable is not set” error. If you start putting voids anywhere on purpose and trying to use them for something meaningful, then you’ll start caring if a branch made them vs. a null…and that would be bad.

The Simple-yet-Powerful Magic of The Loop Result Protocol
(not) Bringing Blankification Back
METHOD and the argument against PROCEDURE