How to Subvert Trashification?

Note: This is a historical thread regarding a time prior to the existence of isotopes, when there was no way to make a successful branch return NULL without foiling the ability to tell from the outside that the branch was taken. So if a branch evaluated to null, it was changed to "trash" (and earlier than that, it was changed to "blank")

>> if true [null]
; trash

>> if true [null] then [print "by being trash and not null, this works"]
by not being trash and not null, this works

This corruption of intent has been solved by in modern times by returning a "boxed null" (or "boxed void") which carries the intent, but "decays" to the unboxed form when assigned to a variable.

Thread preserved for historical purposes.


I've certainly felt the frustration at trashification. If I'm writing something like:

 wrapper: func [x y] [
      case [
          x > y [wrapped x + y]
          x < y [wrapped x - y]
          default [0]
     ]
 ]

If the wrapped function returns a NULL, it is a pain to get that passed through if it's going to get trashed. So this is something where an answer is needed.

Originally all the control constructs had an /OPT refinement so you ended up with CASE and CASE*, and IF and IF*, with the * denoting the specialization that passed through NULL unscathed. But this broke the usual rule I had for the * functions, which meant "lower level function from which the higher level 'user friendly' version is built". Because you can't tell from the outside of a null-passthru CASE* whether a branch was evaluated or not when you see a NULL. It's something the construct itself has to be told to do.

But refinements aren't as flexible as decorating specific branches. That way you can call out which ones you want to subvert trashification and which ones you don't. And it keeps you from complicating the interface with things like /OPT refinements that lead to strange decorated specializations...

 wrapper: func [x y] [
      case [
          x > y :[wrapped x + y]
          x < y :[wrapped x - y]
          default [0]
     ]
 ]

Conceptually, it ties along with the Rebol black-box credo: if you're just passing it through and don't know what the range of values is, you should use a GET-WORD! or GET-PATH!, in case it's an ACTION! or a TRASH. So it's not the most nonsensical thing. Though it would maybe make more "holistic sense" if plain word access of NULL gave back TRASH, while GET-WORD! access gave null. (?!) But I don't think that's a good idea.

Tangent aside: this does mean that if your branch is in a variable, you'll have to DO it:

 >> branch: [print "nulling!" null]

 >> compose [case [true :[do branch]]]
 nulling!
 ; null

Or COMPOSE. Fortunately, GET-GROUP!s compose to "getify" their argument, if a GET-type exists.

 >> branch: [print "nulling!" null]

 >> code: compose [case [true :(branch)]]
 nulling!
 == [case [true :[print "nulling!" null]]]

 >> do code
 ; null

Or maybe GET-GROUP! itself is in on the trick, just as-is:

 >> branch: [print "nulling!" null]

 >> case [true :(branch)]
 nulling!
 ; null

But it makes one wonder a bit about using GET-WORD! and GET-PATH! if this is a "soft-quoted branch". If the other GETs did nulling, do they also? For today, case [true :branch] is a synonym for case [true (:branch)] as is usual with soft-quoting. So it might seem odd to make it also mean that if branch is a null variable, or a block of code that evaluates to NULL, then you get the NULL and all bets are off as far as trashification. The only way you could get trashification in that case would be with the (:branch) as above, since the GROUP! is plain.

There are questions, but this provides a possibly coherent train of thought about an issue that I agree is a thorn. It ties into the other benefits of soft-quoted branching, like being able to see the quote mark and not have it disappear before the control construct sees it....

This was written two years ago, prior to isotopes.

At that time, TRASH was a first class value that represented a non-convertible intent from null. When a NULL was changed into trash, it was a conflating distortion of two semantically distinct types.

So there were two potential motivations you might want to mark a branch to subvert this:

  • honest - The @rgchris reason ("don't corrupt my value...it's a NULL and I meant NULL")

  • dishonest - A desire to communicate to an adjoining ELSE-or-THEN-like-construct that although you took a branch and ran code, you didn't take a branch...so it reacts as if you hadn't.

I empathized with the honest reason, and now isotopes have supplanted trashification as the solution. ~null~ isotopes preserve null intent, and I'm pretty sure we're all square on this now.

Somewhere along the line I got the weird idea that the dishonest reason was worth supporting. But allowing it isn't just technically esoteric and weird... it also breaks the invariant of knowing from the outside of a conditional construct whether it ran code or not.

You might want to build higher-level conditional constructs out of lower-level ones... much the way I've shown how to build loops on top of multiple loops... with pure NULL uniquely signaling breaks. If a user could pass in a kind of branch that broke the rules, your higher-level conditional won't be able to function correctly.

So no more empathy for the dishonest purpose! We can forget about the idea of an annotation to say that a branch should run and return pure null, and instead use smarter things...like input-decaying *ELSE and *THEN!

This opens up better meanings for the @[...] branching, and there's already something on the table for that.

1 Like

I realized there's a new and interesting mechanism we can throw in here for the "honest uses"... that is to go through ^META mechanisms and then UNMETA it.

This is enabled by the new rules, where no ^META type is either NULL or VOID.

>> ^(null)
== '

>> ^(void)
== ~

As I explain in the linked post, I had once thought that making ^META of NULL be NULL had an advantage, by letting things like ELSE react to a NULL despite its containment . Yet having it be purely contained means that NULL and VOID are available for other meanings.

So CASE has it avaliable unambiguously for signaling purposes!

wrapper: func [x y] [
    unmeta case [
        x > y ^[wrapped x + y]
        x < y ^[wrapped x - y]
    ] else ^[
       0
    ]
]

The branch form ^[...] is supported by all branching constructs. But we could also imagine CASE itself offering a ^META level overall by having using a meta block for its cases list:

wrapper: func [x y] [
    unmeta case ^[
        x > y [wrapped x + y]
        x < y [wrapped x - y]
    ] else ^[
       0
    ]
]

While this won't likely be Chris's favorite method of doing things...it is interesting because it's an option that actually works, under my "honest" standard set above. It acts with fidelity to running the ELSE on only cases where no branch gets taken.

1 Like