The Truthiness of Nothing

Does it really add value to consider the nothing state (an antiform blank held by unset variables) to be neither truthy nor falsey?

Here's a quick survey of how UNSET! (the most nothing-like thing in historical Rebol) is handled:

Rebol2

rebol2>> either get/any 'asdf [print "truthy"] [print "falsey"]
** Script Error: either is missing its condition argument

rebol2>> unset? all [get/any 'asdf]
== true

rebol2>> unset? any [get/any 'asdf]
== true

rebol2>> case [get/any 'asdf [print "truthy"]]
** Script Error: Block did not return a value

R3-Alpha

r3-alpha>> either get/any 'asdf [print "truthy"] [print "falsey"]
** Script error: either does not allow unset! for its condition argument

r3-alpha>> unset? all [get/any 'asdf]
== true

r3-alpha>> unset? any [get/any 'asdf]
== false  ; ...huh?

r3-alpha>> any [get/any 'asdf 1020]   
== 1020

r3-alpha>> case [get/any 'asdf [print "truthy"]]
** Script error: block did not return a value

Red

red>> if get/any 'asdf [print "truthy"]
truthy

red>> either get/any 'asdf [print "truthy"] [print "falsey"]
*** Script Error: block did not return a value  ; <-- huh?

red>> unset? all [get/any 'asdf]
== true

red>> unset? any [get/any 'asdf]
== true

red>> case [get/any 'asdf [print "truthy"]]
*** Script Error: block did not return a value  ; <-- huh, again?

Oldes Rebol3

Transparent in ANY and ALL (like a Ren-C void), but presumably still an error in plain IF or EITHER or CASE...

Ren-C's "Always Error" Has Been More Consistent, But...

Clearly people have been pushing away from it being an error.

So does being "ornery" for conditional logic really help matters? When we consider the dual role of nothing as an "uninteresting, but successful" result, might we get as much (or more?) value from considering it to be always truthy?

For example: Ren-C uses NOTHING as the result of PRINT when the print actually produces output. Otherwise, you get NULL.

>> message: "Hello"
== "Hello"

>> print message  ; won't have a console "==" due to being a nothing result
Hello

>> nothing? print message
Hello
== ~true~  ; anti

>> message: null
== ~null~  ; anti

>> print maybe message
== ~null~  ; anti

>> print []
== ~null~  ; anti

Nothing has the property of suppressing console output, which is desirable in most print cases--at least those that do print output. And if you said (x: print "Hello") you'd get a variable that would create an error on access, which seems also desirable.

If nothing was truthy, that makes it easier to act on the nothing-vs-null distinction in something like an ANY or ALL construct.

Should "Meaningless but Truthy" = "Unset Variable State"?

A related question may be if functions like PRINT should be returning the same value as what is held by an unset variable.

Now that non-antiform BLANK! itself is truthy, might it be a better choice?

>> print "Hello"
Hello
== _

But then...

  • For the visual we seek, the console would need to not print BLANK!. But I find it unsatisfying to have a non-antiform be what has "no representation".

  • If you assigned the result of this "meaningless" value to a variable, you wouldn't have that added protection that the variable would appear unset.

Truthy Nothing Seems To Have More Pluses than Minuses

My "semantic safety" bias initially had made me think that when you have a function like PRINT, it's nice to catch potential mistakes when you tried to act like it was a function that could meaningfully be tested for some kind of logical result. So I pushed R3-Alpha's error from IF and EITHER further into ANY and ALL.

Then I went and made it so that when the PRINT received an opted-out input, it gave back NULL instead of NOTHING. :face_with_diagonal_mouth: So it was something you could act on with ELSE, but not other conditional constructs.

The evolution from UNSET! to the blank-antiform that is today's NOTHING has been a long and winding one. Working around its ornery-ness gave rise to all kinds of interesting designs like voids, and invisibles like ELIDE PRINT.

But though I'm sure that I haven't considered all the angles yet...having nothing be neither-true-nor-false is looking more like a dying historical artifact than something with a clear motivation applicable to the present.

Nothing being always truthy offers consistency...and it's possible to ELIDE it to get "no vote" so it won't affect an ANY or ALL (the way Oldes R3 treats unsets). Yet having the vote isn't entirely useless either. I can't think of a whole lot of downside, so I think it's worth trying.


A better axis of orneryness that may actually catch more problems in practice is: Should Nothing be Illegal in Comparisons

(I point out in that discussion that simply disabling the ability to check nothing for truthiness/falseyness is kind of a strange counterpart to a routine that returns--say--an INTEGER! in all cases, where you get zero information from testing an integer conditionally but without anything to stop you...making the disablement of testing nothing conditionally seem like a fairly empty gesture.)

1 Like

Having let things settle around in my head for a bit, I think this is the case. And it's a rare occasion of Ren-C coming into alignment with a change to Rebol made by Red. (Though they apparently do not run through common code to test conditionals, since EITHER and IF disagree.)

Also the parallels are inexact, since they have fewer parts...and use UNSET! for things Ren-C does not use NOTHING for.

For instance, NIHIL:

red>> if comment "like this" [print "for example"]
for example

ren-c>> if comment "like this" [print "for example"]
** Script Error: No value in antiform BLOCK! pack: ~[]~ (nihil)

And then Ren-C has VOID:

red>> if (if false [<a>]) [print "for example"]
== none

ren-c>> if (if false [<a>]) [print "for example"]
** Script Error: Invalid use of void

Comparison To JavaScript Undefined

When you think about language mapping, it seems that NOTHING maps to JavaScript's undefined. But JavaScript makes undefined falsey:

>> if (undefined) {console.log("Them's the breaks.")}
<- undefined

But I think I put together a pretty good rationale for the choice:

Still, it's probably worth doing a little "Nothing Philosophy" just to make sure the type is understood.

To try and characterize nothing, we can look at the places it appears:

Contents of an unset variable

>> asdf
** Script Error: asdf is ~ antiform (see ^(...) and GET/ANY)

Result of a FUNC that reaches its end but does not use RETURN

append-two: func [block value] [
    repeat 2 [append block value]
]

>> nothing? append-two (block: [a b c]) [d e]
== ~true~  ; anti

>> block
== [a b c [d e] [d e]]

Lacking a RETURN as a final statement doesn't mean a function always returns nothing, as there may be other control paths in the function that use RETURN to give different results.

Anything That Doesn't Want == Result In The Console

Ren-C has a big palette of choices to paint another choice for this. It seems awfully tempting to use the ~void~ antiform.

>> compose [1 (if false [2]) 3]
== [1 3]

>> if false [2]

>> if true [2]
== 2

The void just vaporized completely. Doesn't that seem nice and consistent to have it vaporize in the console as well? (By contrast, NOTHING is a BLANK! antiform, and causes errors when you try to COMPOSE it into a block.)

I definitely thought that for a while. But the catch is that VOID is way too nice about opting out of operations. If everything that didn't want to show a console result started returning VOID, you'd have silent opt-outs of places that really should have been erroring on a "meaningless" value.

So it begins to make sense that you show ~void~ antiforms (which offers a concrete education about what the result is)... and you don't show the "meaningless" value. PRINT returns NOTHING precisely because it is not an important return result. And if it's not important, why clutter the display with it?

So far, so good. But the conflation of the "meaningless" value with what is used for UNSET! variables is where it maybe looks not so good.

>> get/any $asdf

>> get/any $asdf

>> print "WHAT THE HECK IS THE VALUE OF ASDF?  IS IT VOID?  IS IT NIHIL?"
WHAT THE HECK IS THE VALUE OF ASDF?  IS IT VOID?  IS IT NIHIL?

This might lead us down a line of thinking that we do need something like an ~unset~ antiform that is distinct. (Or we might turn our head around funny and say unsetness keeps ~ NOTHING and anything that wants to be invisible uses an ~invisible~ antiform.)

Here's where I guess I'm just going to have to say "trust me, it's not worth it" (and I've tried it all). You can make peace by saying that a variable holding nothing is meaningless. If you tried to type it in the console, you'd get an error:

>> asdf
** Script Error: asdf is ~ antiform (see ^(...) and GET/ANY)

The fact that you get an error relaxes the concern over it not displaying anything if you do manage to get at its value. If you sneak past the error raising then you'll find the variable is... meaningless. Why display anything?

There are other ways to make peace here, like imagining fancier graphical consoles that animate a little == ~ ; anti and then it fades away. Or you check a box somewhere and it shows you all the == ~ ; anti

Nothing is NOT Used To Poison Assignments

Worth noticing is that UNSET! can't be assigned to SET-WORD! or SET-PATH! in Rebol2, R3-Alpha, or Red.

>> x: print "Rebol2"
Rebol2
** Script Error: x needs a value

red>> x: print "Red"
Red
*** Script Error: x: needs a value

>> x: print "R3-Alpha"
R3-Alpha
** Script error: x: needs a value

But in Ren-C, assigning NOTHING is allowed... and even considered a quick way to unset a variable.

>> x: print "Ren-C"
Ren-C

>> unset? $x
== ~true~  ; anti

>> y: 10 + 20
== 30

>> y: ~

>> unset? $y
== ~true~  ; anti

What you can't assign is NIHIL, an empty multi-return PACK...because there's nothing to extract:

>> 1 + 1 comment "Hi"
== 2

>> x: comment "Hi"
** Script Error: No value in antiform BLOCK! pack: ~[]~ (nihil)

I think that the historical choice of PRINT returning UNSET! was driven more by the desire to not show anything in the console than it was specifically about trying to avoid assignments to variables. But it was likely considered a bonus, because it would catch unintended assignments.

I don't have any misgivings about allowing SET-WORD! to assign NOTHING. You might lose some error locality. But there's room in the cell for a symbol... if nothing was returned from a function, the name the function was called under could be poked into the nothing in case that helped later.

>> x: print "Save the symbol"
Save the symbol

>> label of get/any $x
== print

>> x
** Script Error: x is nothing (~ antiform), was generated by PRINT    

(I am now having nightmares about people exploiting that to sneak data into places.)

Nothing Can't Be Used In Comparisons

If your function ever returns NOTHING, then it likely can't be used in comparisons (consistent with Rebol2 treatment of UNSET!, but not R3-Alpha or Red, which allow comparing UNSET! with EQUAL? etc).

But NOTHING can be tested for truthiness, against a NULL result (or ~false~ antiform).


So Any Big Conclusions Here?

I think a general rule is that unless your function is is proxying arbitrary results (like a DO or similar) then if it originates a return value of NOTHING on its own, it should always return NOTHING (for that configuration of refinements). If it originates any other value, it should probably only be NULL.

This is just based on the idea that you don't want to be throwing callers for a weird loop by having one of many different possible results, where one of them will mysteriously unset their variables or wreck a comparison.

So the only thing you have left as an option is a conditional truthy test: meaningless result that says you succeeded, or a NULL result that means you couldn't do it. PRINT is a good example of that.

(I recently made CALL without /RELAX return NOTHING and I think that's a very good example to help illustrate the premise.)

It's all making sense in my head... for now...