What Are NULL, VOID, and TRASH?

These are three important "antiforms" you need to know about. (Antiforms can be held in variables, but cannot be put into lists like BLOCK!).

TRASH is the antiform of BLANK!

Trash is used as the contents of an unset variable:

>> x: ~  ; will unset the variable

>> unset? $x  ; in the modern world ('x) makes a word with no binding
== ~okay~  ; anti

Its meta-representation is a quasiform shown as a lone tilde (~), which you can call "quasi blank" if you like (I also call it a "quasar"). So evaluating quasiform blank gives you antiform blank, e.g. TRASH, which has no representation in the console.

>> quasi _
== ~

>> quasar? first [~]
== ~okay~  ; anti

>> ~

>> trash? ~
== ~okay~  ; anti

For reasons that are a bit beyond the scope of this post, TRASH cannot be passed as a "normal" parameter to a function. It represents the state of a parameter being unfulfilled in a FRAME!.

>> make frame! append/
== #[frame! [
    series: ~
    value: ~
]]

This aligns with some of what Carl wrote about in UNSET! is not first class, and among the implications are that operations like comparison functions do not accept unset states... as in Rebol2:

rebol2>> #[unset!] = 1
** Script Error: Cannot use equal? on unset! value

ren-c>> ~ = 1
** Error: VALUE1 argument of EQUAL? is unspecified

VOID is the unstable antiform of an empty BLOCK! (PACK!)

VOID vanishes in REDUCE and acts as a no-op for things like APPEND. It is the result of eval [] and vaporizes in COMPOSE/etc.

>> void
== ~[]~  ; anti

>> compose [a (if 10 > 20 ['b] else []) c]
== [a c]

>> reduce [1 + 2, if 10 > 20 [<nothing>] else [], 10 + 20]
== [3 30]

>> append [a b c] void
== [a b c]

>> if 10 > 20 ['b] else []
== ~[]~  ; anti

VOID will opt out of ANY and ALL. But because of this, an isolated conditional like IF can't make a logically consistent decision about it being a "branch trigger" or "branch inhibitor", it gives back an error:

>> if 10 > 20 [<foo>] else []
== ~[]~  ; anti

>> any [if 10 > 20 [<foo>] else [], 10 + 20]
== 30

>> all [10 + 20, if 10 > 20 [<foo>] else []]
== 30

>> if (if 10 > 20 [<foo>] else []) [20]
** Error: ~[]~ antiform cannot be used in isolated conditional expressions

NULL is the antiform of the WORD! "null".

In the API this is represented as the 0 pointer and does not require having its handle released, so it is like C's NULL. It is used as an "ornery nothing"...but unlike TRASH it doesn't indicate an unset variable, so it can be fetched by normal WORD! access. The system accomplishes elegant error locality using the VOID-in-NULL-out protocol in many places, which hinges on the MAYBE function that converts NULL to void.

>> case [1 > 2 [<a>] 10 > 20 [<b>]]]
== ~null~  ; anti

>> reduce [1 + 2 case [1 > 2 [<a>] 10 > 20 [<b>]]] 10 + 20]
** Error: can't put ~null~ antiforms in blocks

>> reduce [1 + 2 maybe case [1 > 2 [<a>] 10 > 20 [<b>]]] 10 + 20]
== [3 30]

>> third [d e]
** Script Error: cannot pick 3

>> try third [d e]
== ~null~  ; anti

>> append [a b c] try third [d e]
** Error: Cannot put ~null~ antiforms in blocks

>> compose [all your base (try third [d e]) are belong to us]
** Error: Cannot COMPOSE ~null~ antiforms into slots

>> maybe third [d e]
== ~[]~  ; anti

>> append [a b c] maybe third [d e]
== [a b c]

To Sum Up...

  • TRASH is the contents of an unset variable

  • VOID is intentional emptiness--tolerated many places as meaning "I'd like to opt out please"

    • Since it opts out of aggregate conditional tests, it can't logically be acted on in an isolated conditional expression like IF
  • NULL is a signal, often meaning "I couldn't find what you were looking for"

    • Because it is a kind of "soft failure", it is the (only) conditionally false value

    • Also because it is a soft failure, most non-conditional slots reject it as an argument

    • MAYBE can be used tactically to convert NULL results to VOID

NULL and TRASH can be held in variables or API handles, but won't be found in lists like BLOCK!.

VOID is an unstable antiform, and can't be saved in variables--only meta-represented.

1 Like

F.A.Q.

How Did These Types Evolve From Rebol2 UNSET! + NONE! ?

See this thread.

Isn't It Simpler to unify VOID+TRASH as UNSET!, like Rebol2?

One can reasonably ask if when all is considered, having fewer parts is better--even when more parts can be shown to have "a benefit here and there".

So this does hinge on how you feel about things like:

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

Personally I see too much downside to conflating "meaningless" values with "meaningful opting out". While you might be able to make excuses for an oversimplified example that's just PRINT like this one, when you imagine a more complex expression, an error is far preferable.

So in Ren-C you have to erase the trash value, with something like:

>> compose [a (elide print "b") c]
b
== [a c]

Might ~ Antiforms Be The Better Representation for Void?

Given how tilde is just about as light of a token as you can get, one might think the vanishing intent deserves that notation. There could be a switcheroo where ~trash~ was used for the unset variable state, and ~ was used for void.

But in practice, unset variables are far more common... and the super-light notation for creating an unset variable as (var: ~) is just too useful.

Could TRIPWIRE! Replace TRASH?

Ren-C has another type that causes errors on variable access, which is the antiform TAG!, a.k.a. TRIPWIRE!

>> var: ~<some message>~
== ~<some message>~  ; anti

>> var
** Error: var is a tripwire: some message

So it may seem redundant to have antiform blanks doing the same thing. If everywhere that produces TRASH! today made TRIPWIRE!, then antiform blank could more readily be repurposed as VOID.

Mechanically, yes, this can be done (I tried it). But so many of the cheap uses of ~, e.g. unsetting variables... or return: [~] in specs to indicate no result have to be reimagined. There's a rather deep dependency on antiform ~ being an ornery state. Believe me: if I could get rid of one of these forms, I would.