Historical Note: This post discusses why what was called VOID at one point was ultimately chosen to be renamed as NULL. To keep the thrust of the point coherent, the terminology has been left as-is. Just know that for a time, what is known as NULL today was called VOID.
Being meticulous about getting "void" right vs. "UNSET!" has been paying off tremendously. The swampy nature of dealing with such issues in Rebol2/R3-Alpha/Red have given way to clarity...and subsequently, enabled great and solid features.
Now a new bonus:
If voids are always NULL in the API, there's a huge win
Check out this libRebol pattern, taking advantage of OPT and TRY for conveniently wrangling the void/blank switcheroos:
REBVAL *var = ...;
REBVAL *obj = rebRun(
"opt match [object! blank!] try case [",
var, "= some/value [first foo/baz/bar]",
"integer?", var, "and (mode = 'widget) [second mumble]",
"]", END);
if (!obj) {
// leverages C's natural "NULL is falsey" property
// so testing for success requires no extra API call
// nothing to clean up, no handle to rebRelease()
//
... code for failure ...
return;
}
// Rebol code can do heavy lifting for validation / errors
// so we can assume the value is good to go
//
... code for success ...
rebRelease(obj); // if you're done with it...
You let the embedded Rebol pick things apart to make sure the result is a type you care about or not. Then your first reaction to the result can be "did I get something in the set of answers I'm interested in processing or not", and that reaction can be decided without any API call...you just take advantage of NULL being "falsey" in C.
Since people who aren't me haven't really been experimenting all that much with how the "voiding" has been working, you might not be as excited about it as I am. But that's just because you haven't tried it. OPT and TRY getting in there with all the new constructs brings a whole new level to "the game", and this idea of having an easy signal channel back to C from those constructs is really compelling.
"voids" always become NULL, why not call the test NULL?
Remember: type of (do [])
is _
. It's a falsey/blank value to say it has no type. There's no VOID! type, because voids have no unique identity. You can locate several different unique UNSET! cells in various arrays in R3-Alpha, mutate them to other types like INTEGER!, change them back, etc. Not so in Ren-C.
No void "type" means that "changing the name of void" is really just "changing the name of the test for void"... from VOID? to NULL?.
This seems like a win to me. Not only does it reduce the barrier to talking about the C behavior from the Rebol behavior, null
can be the JavaScript representation too. The word isn't taken in Rebol to mean anything else, so why not reduce the cognitive load by using what other languages use?
Same number of letters in VOID and NULL. NULL? vs VALUE? have different first letters, which may be a plus.
I talked about this before, so why didn't I do it sooner?
Ren-C eliminated UNSET!-typed cells in ANY-ARRAY!, but the practical mechanics of voids appearing in various places the evaluator see as "incarnated cells" have lingered. Solutions to the problems come along one piece at a time, like the just-now-reconcieved definition of UNEVAL:
uneval: func [
{Make expression that when evaluated, will produce the input cell}
return: [group!]
{`()` if void cell, or `(quote ...)` where ... is passed-in cell}
cell [<opt> any-value!]
][
either void? :cell [quote ()] [reduce quote ('quote cell)]
]
So if you do compose/only [if void? (uneval :some-unset-var) [print "this prints"]]. Necessity is the mother of invention for these kinds of things, and so far they've filled in the gaps. Things are at a technical point where we can do it.
UPDATE Jan 2019: UNEVAL--and the reasons it had to be invented--motivated Generic Quoting, which has supplanted it. Interestingly,
(foo: ')
uses generic quoting to assign the absence of a value to foo--and hence unsets it.
There's still going to be the tradeoff I mentioned regarding the mutability of handles. If you have a pointer to a REBVAL*
now which is INTEGER!, might some operation change it to a STRING! later? Or if it's a BLOCK!, might its index be changed in place vs. making a new block?
Saves on handle allocations. But now imagine that you've handed a cell out to the user through the API, and offer them an API for doing evaluations into that cell vs. making a new one. And that evaluation produces void. That's a transition they wouldn't be able to do under this model. This makes an API handle's "slot" more like an array cell than it is like a context variable...it can hold blanks, just not voids.
Having had time to work with matters in practice and looking at the big picture, this is a small price to pay.