Why Are TRASH and VOID Distinct In Ren-C?

It appears that Ren-C uses VOID! for opting out of things like COMPOSE, and TRASH! for what things like PRINT return--to show no value in the console.

But Rebol2, R3-Alpha, and Red use UNSET! for both:

 rebol2>> unset 'foo
          ; <-- no `==` result shown (unset!)

 rebol2>> compose [a (get/any 'foo) c]
 == [a c]

 rebol2>> print "Hello"
 Hello
          ; <-- no `==` result shown (unset!)

 rebol2>> compose [a (print "Hello") c]
 Hello
 == [a c]

All things being equal, one state to represent "nothing" would seem better than having more than one.

So are the benefits of having two different states worth it, vs. the simplification of only having to worry about one state?

1 Like

That might seem okay if you deliberately unset a variable. But I'll point out that the same thing happens with variable names that are garbage, in all Redbols:

 rebol2>> compose [a (get/any 'asdfkjkl) c]
 == [a c]

 red>> compose [a (get/any 'asdfkjkl) c]
 == [a c]

 r3-alpha>> compose [a (get/any 'asdfkjkl) c]
 == [a c]

Ren-C's full answers to binding issues are beyond the scope of this post. But it does point to the idea that having too few states in play can lead to undesirable results.

It may be interesting to know that early Ren-C's focus was not on separating the states. So it also only had one state for this... which we might retroactively call "VOIDNULL".

The principal difference was on ensuring that VOIDNULL could never be found in a BLOCK!. It was a state that could only be held by variables. This was to try and fix up inconsistencies in historical Rebol... where some code paths let you put the unset state in blocks, others not, representational issues, etc.

rebol2>> compose [a (get/any 'asdfasdf) b]
== [a b]

rebol2>> append [a b c] reduce [get/any 'asdfasdf]
== [a b c unset]

But beyond preventing VOIDNULL in blocks, not much else changed. Failed branching constructs produced this state as well. So when an IF didn't take its branch, you could use it to unset a variable... as well as to make COMPOSE slots vanish. It didn't print in the console either!

historical>> x: if 1 > 2 [<math-is-broken>]  ; prints nothing on next line

historical>> x
** Error: whatever the error for an unset variable is

historical>> compose [a (if 1 > 2 ['b]) c]
== [a c]

And then there was what we could call "LIT-VOIDNULL!". This was what I thought made perfect sense, to make it single-quoted nothingness:

historical>> x: '

historical>> x
** Error: x is VOIDNULL

So VOIDNULL wasn't the tick mark... it was what you got when you evaluated the tick mark. This fit in with things like LIT-WORD! and LIT-PATH!, where the evaluator drops one level of quoting. But here dropping that level gives you the "missing" state. Having the console show no output seemed to fit in perfectly.

And it is kind of clever. However...

Then Isotopes Happened...

The rise of generalized isotopes meant there wasn't merely a single state that couldn't be put in blocks...but a whole menagerie of antiforms.

To fit into this menagerie correctly, the state that evaluated to produce an unset variable had to be a quasiform to make an antiform...not a quoted. So a lone apostrophe evaluated to a non-antiform--known as a BLANK!, which can be put in blocks and causes commas to appear:

https://rebol.metaeducation.com/t/blank-the-worlds-weirdest-comma-mechanic/1387

>> '
== 

>> append [a b] '
== [a b,]

Then, the lifted representation of a "VOID!" became the quasiform of a BLANK!... a lone tilde:

>> quasi '
== ~

>> x: ~
== |~|  ; antiform (void!)

>> x
** Error...

That quasiform evaluates to the antiform. Though it can be written out as |~| it could also be purple, or anything you like.

For a time, the console just said this would display nothing.

First Reason For The TRASH / VOID Split Was Safety

For a time, VOID! did double-duty, as the contents of an unset variable... but also the return value of a function with a "meaningless" result.

So functions like HELP and PRINT returned VOID!.

But this meant meaningless intent would also vanish:

>> append [a b c] print "This seems like it should error"
This seems like it should error
== [a b c]

So the idea of making TRASH!--a type that is more ornery--came along.

>> append [a b c] print "With trash, it does error"
With trash, it does error
** Error: APPEND doesn't allow TRASH! (~<print>~) for its VALUE argument

Second Reason For The Split Was Needing Unstable VOID!

If the arguments for safety aren't convincing, there's a very important distinguishing characteristic of VOID!... which is that it's an unstable antiform.

VOID can be used not just for erasures of content in places like COMPOSE or arguments to APPEND, but it can be used for the erasure of variables themselves (when relevant).

For instance: removing a key from a MAP!:

>> m: to map! [a 10 b 20]

>> m.a: ()
== |~|  ; antiform (void!)

>> m
== #[map [b 20]]

There's competition in situations like this between "mapping to trash" and "not being there at all". And the unstable antiform state makes it cleanly suited to whenever you want to capture the state of not being there at all.

So... Is It Worth It?

IN THE CURRENT DESIGN, I CAN UNEQUIVOCALLY SAY YES.

But it wasn't always quite perfect. For some time, empty pack was used to implement VOID... and "HEAVY VOID" was just another antiform WORD! (~void~) that could be stored in variables... just like TRASH.

I could tell from my own day-to-day usage that this "heavy void" earned its keep as a distinct type from TRASH!. But being able to be stored in variables led to some tough-to-answer questions about its behavior. For instance: was it a good or bad idea to allow direct access to ~void~ via WORD!, or should it error like TRASH ?

Or consider something like DEFAULT. It was defined such that it would overwrite null and trash variables:

>> x: ~

>> x: default [print "Foo" 1 + 2]
Foo
== 3

>> x
== 3

>> x: default [print "Bar" 10 + 20]
== 3

But what about ~void~ antiforms? Were those also things that had to be overwritten? It wasn't clear.

Basically, with HEAVY VOID as a stable antiform wound up creating the very combinatoric questions one might be concerned about from having "too many types". Though it was different in name, its fundamental properties weren't different enough to make decisions flow obviously about how its handling should be different from TRASH or NULL.

Yet a crucial change of VOID to being the antiform of BLANK! freed up the empty antiform BLOCK! state to be a non-vanishing, unstable antiform... the heavy void.

This pulled everything together, and the clarification in how decisions are able to flow now is rather mind-blowing.

:man_mage: :compass: