The Truthiness (or Ignorability?) of Trash

UPDATE 2026: This issue has been resolved in a way that now seems "obvious" in hindsight. What made it less obvious was that ~ produced TRASH!, and was being used for "unset" variables. The recognition that ~ should be antiform BLANK! (VOID!) and not antiform RUNE! helped push all the pieces together... to where any {x: ~ ...} could usefully skip the void expression. TRASH! became an unstable antiform and many other behaviors pinned down.


Does it really add value to consider the "TRASH" state (antiform RUNE!) to be neither truthy nor falsey?

Here's a quick survey of how UNSET! (the most trash-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...

UPDATE: Oldes has committed a change making UNSET! truthy in 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 trash 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 TRASH 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 trash result
Hello

>> trash? print message
Hello
== ~okay~  ; anti

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

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

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

TRASH 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 trash was truthy, that makes it easier to act on the trash-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.

Truthy Trash 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 TRASH. :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 SPACE-antiform that is today's TRASH 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 trash be neither-true-nor-false is looking more like a dying historical artifact than something with a clear motivation applicable to the present.

Trash 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 Trash be Illegal in Comparisons

(I point out in that discussion that simply disabling the ability to check trash 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 trash conditionally seem like a fairly empty gesture.)

1 Like

It's probably worth doing a little "Trash Philosophy" just to make sure the type is understood.


TRASH is used as the contents of an unset variable (like an undefined in JavaScript, perhaps). It is also the result of many functions that have no meaningful result to return, such as a PRINT statement (unless you pass PRINT a VOID in which case it returns NULL). Trying to reference a variable holding this state will trigger an error, and special operations must be used to test for or extract the state from a variable.


Note: JavaScript makes undefined falsey:

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

To try and characterize trash, 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 PROC(EDURE) that reaches its end

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

>> trash? append-two (block: [a b c]) [d e]
== \~okay~\  ; antiform

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

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 VOID.

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

>> if null [2]

>> if okay [2]
== 2

The void just vaporized completely in the COMPOSE. Doesn't that seem nice and consistent to have it vaporize in the console as well? (By contrast, TRASH is a SPACE 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 TRASH 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 meta $asdf

>> get meta $asdf

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

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 ~ TRASH 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 trash 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

Trash 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 TRASH 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

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 TRASH. You might lose some error locality. But there's room in the cell for a symbol... if trash was returned from a function, the name the function was called under could be poked into the trash in case that helped later.

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

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

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

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

Trash Can't Be Used In Comparisons

If your function ever returns TRASH, 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 TRASH can be tested for truthiness, against a NULL result


So Any Big Conclusions Here?

I think a general rule is that unless your function is is proxying arbitrary results (like an EVAL or similar) then if it originates a return value of TRASH on its own, it should always return TRASH (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 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 TRASH and I think that's a very good example to help illustrate the premise.)

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

So there's also the question of ignoring trash.

all {
   x: y: z: ~
   blah [blah [blah blah x: ... y: ...]]
   blah [blah [blah x y z: ....]]
}

The ALL doesn't trip over the trash result. And that's good.

But ignoring it may be better, e.g. to use this with ANY:

any {
   x: y: z: ~
   blah [blah [blah blah x: ... y: ...]]
   blah [blah [blah x y z: ....]]
}

So I'm definitely considering the switch to "no vote". But there are a few competing interests.

You can always write:

any {
   elide x: y: z: ~
   blah [blah [blah blah x: ... y: ...]]
   blah [blah [blah x y z: ....]]
}

The set-words will still be gathered.

After Stepping Away And Coming Back, I See It Clearly

TRASH! Should Be Neither Truthy Nor Falsey

The reason is, that TRASH! is what you get when you haven't defined a return result for your function yet. You haven't committed it to "vanishing" semantics with a VOID!. You haven't committed to NULL. You just wrote the function as a PROC, or a FUNC with return: [trash!]

Effectively, you have not made a contract with your callers yet. You haven't promised you'll never return anything.

The very pleasing idea we now have is that ~ is quasiform VOID!, not TRASH!

So the assignment is thrown out by the ANY.

1 Like

Things have changed, so it's worth doing a bit of reflection...

TRASH! is now an unstable antiform. Furthermore, that's become intrinsic to its "not-true, not-false" status... as only stable values can be conditionally tested for truthiness and falseyness.

However, to keep things working at first... trash was granted an assignment exemption. So even though it's unstable and you have to use ^var to fetch it, you could still (for the moment) write:

>> var: ~

>> print "There was no output because it was TRASH!"
There was no output because it was TRASH!

But it may not be your favorite choice as an "unset state". You can't use it to opt out of things:

>> append [a b c] ^var
** PANIC: Cannot decay TRASH! to a stable value

Whereas if you'd chosen a VOID!, you could have opted out:

>> var: ()
== \~,~\  ; antiform (void!)

>> append [a b c] ^var
== [a b c]

Also you would have gotten the nice "opts out in ANY and ALL" aspect mentioned above.

Different Kinds of "Unset"... Which To Choose?

TRASH! has a feature you don't have in VOID!, which is to be able to name the trash whatever you want... to provide a better signal of "why is this unset".

Whereas VOID! has this ability to opt out and vanish without requiring any transformation (when passed to routines that treat voids as vanishing).

The old question returns: with them both being unstable now, what would be the downside of unifying VOID! with TRASH!?

The case that has historically killed it for me has been this one:

>> append [a b c] (... print "Hi")
== [a b c]

I still feel like we need this poison status. I also don't think that "don't print in console" and "vaporize silently" are the same intent.

So Should TRASH! Assignments Have To Be ^META?

This would bring back a maybe-good thing, that you couldn't accidentally assign the results of a function like PRINT:

>> x: print "this would panic"
this would panic
** PANIC: Can't assign X TRASH! antiform ~#[print]#~ (use ^X)

>> ^x: print "this would not panic"
this would not panic

This wouldn't be that much of a tax on purposeful assignments, but it would mean that (x: ~) as a pattern would become more of a hassle.

I'm kind of torn on it. It's hard because accidental assignments of trash don't seem that common, but if you stopped them for new users that might be helpful.

There's a finesse that can let us gloss over the difference of trash and ghost somewhat...

This is the idea that using leading colons as :var will convert anything that would have looked up to a VOID! into a null.

But this leads one to ask... why not have it also convert anything that would have looked up to a TRASH! into a null?

This gives you the ability to not care which form of "unset" something chooses to be... including NULL itself.

You can't get a TRASH! to opt-in-with-nothing directly with ^trash-var... it's deliberately "trashy" to stop such behavior.

...but this means you're just one step away with opt :trash-var.

I think that's a sufficient level of intentionality to say you're not getting optional behavior on accident.

So the odd answer here is that TRASH! and VOID! are "one small colon away from falseyness"!

I'm a big fan of the clarifying rule that all stable antiforms can be tested LOGICAL-ly... because it helps us realize why VOID! and TRASH! cannot.

>> logical ~
** PANIC: (VOID! is an undecayable antiform and LOGICAL can't test it)

>> logical ~#~
** PANIC: (TRASH! is an undecayable antiform and LOGICAL can't test it)

ACTION! can decay to FRAME!, and PACK! can be tested if it can decay.

I think equality and comparison can move to the stable realm as well, thus explaining why you can't do equality tests on TRASH!...but this implies you can't for VOID! either...

There is some value in being able to test equality against VOID!:

>> if ~ = ^void [print "This isn't *useless*"]
This isn't *useless*

But I think the greater value emerges when we say things like equality and comparison happen on the stable plane.

^META-parameters should be in the minority. We don't want meta-ness creeping around in the system too far. And once you say "well, equality can test for it..." then suddenly anything that wants to be equality-like has to take its argument ^META as well for completeness.

That's a bad trade. Better to nip that in the bud and ask you to work around it other ways:

>> if void? ^void [print "We can just asked you to use VOID?*"]
We can just ask you to use VOID?

Note also, you can lift both sides of an unstable comparison and compare the lifted forms:

>> if (lift ~) = (lift ^void) [print "Compare lifted forms, if you need to"]
Compare lifted forms, if you need to

Pursue Stability Where Possible

:wood: