What Are NULL, VOID, NOTHING, BLANK, and NIHIL?

Ren-C has some very carefully designed distinctions of "nothingness"--with subtleties that go far beyond something like JavaScript's null and undefined.

While it might seem overwhelming at first, they are purposefully chosen, and a pleasure to work with in practice. Here's an attempt to run through what they are and how they are used.


VOID is the antiform of the WORD! "void".

It vanishes in REDUCE and acts as a no-op for things like APPEND. It is also the result of an IF that doesn't take its branch, and vaporizes in COMPOSE/etc.

>> void
== ~void~  ; anti

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

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

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

>> if false ['b]
== ~void~  ; 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 false [10]
== ~void~  ; anti

>> any [if false [10] 10 + 20]
== 30

>> all [10 + 20 if false [1020]]
== 30

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

NOTHING is the antiform of BLANK!.

I chose the name NOTHING for the contents of an unset variable:

>> x: ~  ; will unset the variable

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

Its meta-representation is a quasiform shown as a lone tilde (~), named TRASH. So evaluating TRASH gives you NOTHING, which has no representation in the console.

>> quasi _
== ~

>> trash? first [~]
== ~true~  ; anti

>> ~

>> nothing? ~
== ~true~  ; anti

For reasons that I have outlined elsewhere, NOTHING is a branch trigger:

>> print pick ["hello" "goodbye"] 1  ; result is nothing, no console display
hello

>> print maybe pick ["hello" "goodbye"] 3
== ~null~  ; anti

>> if (print pick ["hello" "goodbye"] 1) [<printed>]
hello
== <printed>

>> if not (print maybe pick ["hello" "goodbye"] 3) [<silent>]
== <silent>

Among the implications here, you can safely use PRINT statements in ANY and ALL (though they're not ignored, and if it is the last statement the overall result will of course be nothing... so use ELIDE PRINT if you want it to be no vote).

>> any [print "Hello", null, 1 + 2, 10 + 20]
Hello
== 3

>> all [1 + 2, 10 + 20, print "Goodbye"]
Goodbye  ; nothing result means no subsequent == in console

>> all [1 + 2, 10 + 20, elide print "Goodbye"]
Goodbye
== 30

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 NOTHING 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 try third [d e]
== ~void~  ; anti

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

NIHIL is an empty antiform block

This is a parameter pack with no values in it. This unstable isotope can't be stored in variables or API handles, and can only be handled in its meta form. (Explanation is beyond the scope of this thread...so read the post on modern invisibility to understand why the shade of distinction from VOID is justified.)

If you want an EVAL to vanish (in the spirit of do []) then it does so with a switch passed to it:

>> reduce [1 + 2 eval/vanish [] 10 + 20]
== [3 30]

But without the switch, it will be NOTHING and cause errors in these cases. But if the contents of the block evaluate to void, that overrides the NOTHING result and it will overall evaluate to void, and vanish in these contexts.

>> reduce [1 + 2 eval [if false [<unused>]] 10 + 20]
== [3 30]

To Sum Up...

  • 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 nothingness signal, often meaning "I couldn't find what you were looking for"

    • Because it is a kind of "soft failure", it is treated as conditionally false

    • 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

But then antiform BLANK! comes in to be NOTHING...the "bad check". A variable holding it is considered to not be set, and it trips up access via WORD!.

All three of these states can be held in variables or API handles. And then pure invisibility is built upon a weirder mechanic of NIHIL, which can only be handled by ^META-aware code. You don't need to know how it works to use it (the implementations of COMMENT and ELIDE are trivial in both the main language and UPARSE combinators). But the mechanics are there required to implement them.

1 Like

Here is some history that explains how these states evolved from Rebol2's types.


Rebol2/R3-Alpha/Red Have Two Kinds of Nothing (both reified)

Historical Redbol gives you two main choices for "nothingness"...#[none] and #[unset]... both of which can be found either in variables, or as values in blocks:

rebol2>> block: reduce [none print "print returns unset"]
print returns unset
== [none unset]  ; misleadingly renders as WORD!s

rebol2>> type? first block
== none!

rebol2>> type? second block
== unset!

Using #[none] has the advantage of being "friendly" on access via word, allowing you to write things like:

rebol2>> var: none

rebol2>> either var [print "do something with var"] [print "do something else"]
do something else

But when var contained an #[unset], you'd get an error instead:

rebol2>> unset 'var

rebol2>> either var [print "do something with var"] [print "do something else"]
** Script Error: var has no value

So instead of using var directly, you had to do something more circuitous and pass the word "var" into a special test routine (morally equivalent to today's set? 'var)

Hence #[none] was reached for frequently out of convenience. Yet this convenience came with a cost: it was very easy to accidentally append one to a block, even if its non-valued intent should have conveyed you might not have wanted to add anything at all.

But it's hard to say: sometimes you did want to add #[none] to a block, to serve as a placeholder.

Also, being able to enumerate a block which contained #[unset] values was problematic, because if you did something like a FOR-EACH it would appear that the variable you were enumerating with was itself not set.

Early Ren-C Made Reified BLANK! and non-Valued NULL

One thing that bugged me was that there was no "pretty" representation for a non-valued state in a block... and that #[none] often thus displayed itself as the word none (seen in the example at the top of the post).

So the BLANK! datatype took the single underscore _.

>> second [a _]
== _

>> if blank? _ [print "yep, it's a blank"]
yep it's a blank

>> if not _ [print "blank is also falsey"]
blank is also falsey

And critically, one of the first things I tried to do was rethink the #[unset] state into something that you'd never find in a block, and called it NULL (as well as made it correspond to C/Javascript null in the API):

>> second [a _]
== _

>> third [a _]
; null

Since NULL couldn't be found in a block, it wasn't ambiguous when you got NULL back from a block operation as to whether there was a "null in that position".

But it's still just two things:

  • blank! - A nothing you can put in a block

    • it was logically false
    • it was friendly via word access (no need for GET-WORD!)
  • null - A nothing you couldn't put in a block

    • it was also logically false
    • it was unfriendly via word access (need GET-WORD! for :VAR, or SET? 'VAR)

This put you in a difficult situation for your choices of emptiness when you were dealing with something like:

append block value  ; what nothing state should you use for value?

If you wanted to avoid accidentally appending blanks to arrays, you kind of wanted NULL so you'd get an error. But once you used NULL, you could not write the convenient if value [...] control structure.

Later Ren-C added a separate "ornery" non-Value State

A third state was added to be neither logically true nor false, and that would trigger an error on accessing a variable with it. (I'll whitewash history a bit and say this state was always called "NOTHING", and also always could not be put in blocks.)

This was the new state of unset variables:

>> unset 'x

>> x
** Error: X is an unset variable

>> get/any 'x
== ~  ; nothing

>> if get/any 'x [print "Ornery!"]
** Error: nothing is neither logically true nor false

So NULL now represented a middle ground. It was something that was easy to test for being nothing (using IF) but that was impossible to accidentally put into a block.

This gave you three behaviors:

[1]  >> nothing-value
     ** Error: NOTHING-VALUE variable is unset

[2]  >> null-value
     ; null

     >> append [a b] null-value
     ** Error: APPEND does not allow adding NULL to blocks

[3]  >> blank-value
     == _

     >> append [a b] blank-value
     == [a b _]

WORD! Antiforms Brought Infinite Non-Valued Choices

Eventually the NULL state became an isotope of the WORD! null, so a ~null~ antiform.

It joined ~okay~ as an antiform you could test for truthiness and falseyness.

You'd use the null antiform as the initialization for something you may run some code and find it doesn't assign, and you want to be able to test that.

 directory: ~null~

 for-each [key val] config [
     if key = 'directory [
         if directory [
             fail ["Directory was already set by config:" directory]
         ]
         directory: val
     ]
 ]

VOID Provided a Clean "Opt-Out" Option

An unfortunate sacrifice that had been made in the design was that the "non-valued" status of NULL was chosen to raise attention to an error condition, rather than be an opportunity to opt-out of an APPEND:

>> append [a b] null-value
** Error: This error is the choice that we went with

>> append [a b] null-value
== [a b]  ; would have been another possibility, but too accident prone

Some "strange" things were tried...such as making it so that appending a BLANK! was a no-op, and if you wanted to append a literal blank you had to append a quoted blank:

 >> append [a b] _
 == [a b]  ; hmmm.

 >> append [a b] quote _
 == [a b _]  ; hmmm.

(It wasn't that strange considering appending a BLOCK! would append its contents, and a quoted block was being tried as the way of specifying /ONLY. This line of thinking ultimately led to the designs for the isotopes that solve things like splicing intent, so it wasn't all for naught!)

After invisibles were rethought as NIHIL, the VOID state that could be stored in variables came along as a new piece of the puzzle.

>> void
== ~void~  ; anti

>> meta void
== ~void~

I realized that void was the perfect choice for opting out of operations:

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

>> append void [a b c]
== ~null~  ; anti

As you see above, an operation can return null when it doesn't have another good answer for giving back in case of a no-op. This gives good error locality, since the null won't trigger another opting out unless you explicitly convert the null to a void with MAYBE.

>> append (append void [a b c]) [d e f]
** Error: APPEND doesn't accept ~NULL~ antiform for the series argument

>> maybe null

>> append (maybe append void [a b c]) [d e f]
== ~null~  ; anti

This gives a (seemingly) complete picture

[1]  >> nothing-value
     ** Error: NOTHING-VALUE variable is unset

      >> append [a b] get/any 'nothing-value
      ** Error: APPEND does not allow adding ~ antiforms to blocks
      
[2]  >> void-value
      == ~void~  ; anti

     >> append [a b] void-value
     == [a b]

[3]  >> null-value
     == ~null~  ; anti

     >> append [a b] null-value
     ** Error: APPEND does not allow adding NULL to blocks

[3a] >> append [a b] maybe null-value
     == [a b]

[4]  >> blank-value
     == _

     >> append [a b] blank-value
     == [a b _]
1 Like

A post was split to a new topic: Where to use BLANK vs TRASH

A post was split to a new topic: Should Voided Variables Be Word-Access Friendly?

I’ve read this through a couple of times, but am still not sure I understand it correctly. The discursive treatment in the [second] post is nice for motivating the various types, but confusing for actually getting a handle on how they work.

So, trying to summarise the situation, here’s my understanding at the moment of the various values involved:

  • ‘Blank’: an ordinary datatype inhabited by the single value _.
  • ‘Nothing’: the antiform of _. Throws an error on variable access, or any other attempt to use it. Used to represent unset variables.
  • ‘Void’: the antiform of void. Does not throws an error on variable access, throws on attempts to use it, except with APPEND, where it acts as a no-op. Not sure how this is used.
  • ‘Null’: the antiform of null. Does not throws an error on variable access, and tests falsey in conditionals, but throws an error on other attempts to use it. Used to represent uninitialised variables.
  • ‘Nihil’: the antiform of an empty block, i.e. a multi-return containing no elements. Completely ignored by the evaluator.

Does this seem correct?

1 Like

Note that NIHIL is only ignored in "interstitial slots". If you try to call a function that isn't expecting a NIHIL and pass it as an argument, that's an error:

>> append [a b c] 'd comment "ignored"
== [a b c d]

>> append [a b c] comment "not ignored" 'd
** Script Error: No value in antiform BLOCK! pack: ~[]~ (nihil)

At one point in time, the second worked. For an understanding of why it no longer does, see:

Invisibility Reviewed Through Modern Eyes

Void is used generically in many places when you want things to vanish:

>> compose [<a> (if false [<b>]) <c>]
== [<a> <c>]

Allowing NULL to vanish here would be too liberal and not reveal what were likely errors. If you have something that may be NULL that you want to convert to a VOID if so, you can use MAYBE.

VOID is also is used for opting out of things, using the "void-in-null-out" strategy. Compare:

>> block: ["a" "b"]

>> unspaced block
== "ab"

>> to word! unspaced block
== ab

With:

>> block: []

>> unspaced block
== ~null~  ; anti

>> to word! unspaced block
** Script Error: to expects [~void~ element?] for its value argument

>> maybe unspaced block
== ~void~  ; anti

>> to word! maybe unspaced block
== ~null~  ; anti

With:

>> block: null

>> unspaced block
** Script Error: unspaced expects [~void~ text! block! the-block! issue!]
                 for its line argument

>> unspaced maybe block
== ~null~  ; anti

>> to word! maybe unspaced maybe block
== ~null~  ; anti

Historical Redbol had a lot of people asking that things give NONE! back when they took NONE! in, and this "none propagation" was messy in terms of leading to whole chains which would opt themselves out without knowing where the problem was. Void-in-null-out encourages being more purposeful--you only throw in the MAYBE where you need them.

1 Like

I'm trying to coalesce the various scattered threads explaining these things into a good top-to-bottom explanation. Please feel free to edit to improve the narrative above if you think any of it doesn't come across as clear, and hopefully this can be tied up!

Don't know exactly what the most convincing argument to make is to people who think having this many states is indicative of a problem, vs. a thought-through design...

Alas, I’m still foggy on how these things relate to each other. I think it’ll take actually using Ren-C to clear up the situation in my mind.