The Truthiness of Trash

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 TRASH for.

For instance, GHOST!:

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

ren-c>> if comment "like this" [print "for example"]
** Script Error: No value ~,~ antiform (ghost)

And then Ren-C has VOID:

Comparison To JavaScript Undefined

When you think about language mapping, it seems that TRASH 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 "Trash Philosophy" just to make sure the type is understood.

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 FUNC that reaches its end but does not use RETURN

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

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

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

Lacking a RETURN as a final statement doesn't mean a function always returns trash, 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 VOID.

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

>> when null [2]

>> when okay [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, 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:any $asdf

>> get:any $asdf

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

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

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 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 a DO 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 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 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...