Pure vs. Impure Invisibility: Do We Need Both?

"Invisibles" were conceived 4 years ago...long before BAD-WORD!s, isotopes, ^META parameters, etc.

The terminology needs a bit of an update, because I want to make a distinction between two different kinds:

  • IMPURE INVISIBLITY is when a special state (like a ~void~ isotope) is discarded in contexts where it's assumed to represent an invisible intent.

    Remember that plain BAD-WORD!s (unevaluated) are normal values and can be in blocks.

    >> first [~void~]
    == ~void~

    But an evaluated ~void~ becomes an isotope, and operations like ALL are willing to tolerate that as a signal of invisible intent it should discard:

    >> ~void~
    == ~void~  ; isotope
    >> all [10 + 20, ~void~]
    == 30

    Unfortunately, this is extra work that ALL must do, since ~void~ isotopes are valid evaluative products. Every construct that wants to integrate impure invisibility bears the burden.

  • PURE INVISIBILITY is an evaluator feature when a function call can truly erase arbitrary code, such as to the right of a SET-WORD!:

    >> y: elide (1 + 2 print "Erased!" 3 + 4) 10 + 20
    == 30
    >> y
    == 30

    This can only be done when a function like ELIDE specially says that it is not expected to return any result at all. Otherwise it's not clear whether y: is supposed to get the ~void~ isotope or something after it.

    (Over time I embraced the odd idea that just saying RETURN and not giving it an argument would be the way of being purely invisible...which has the interesting property that (return some-other-function ...) can actually chain cases where the other function is sometimes purely invisible and sometimes not! This insight arose because of the frustrating fact that C/C++ cannot do such chains... if you say return some_other_function(...); and then change the other function from returning int to void, it will complain that you cannot use return with an argument inside of void functions...even if that function itself returns void.)

Why Not Make "Impure" Invisibility Act "Pure" ("Semipure?")

The existence of the ^META types and operators raises an interesting theoretical option...that a ~void~ isotope could be treated as pure by the evaluator, and it's your responsibility to use meta operations if you wanted to see it:

>> 1 + 2 ~void~
== 1 + 2

>> x: ~void~ 1 + 2
== 3

>> x
== 3

>> y: ^ ~void~ 1 + 2
== 3

>> y
== ~void~
    ; ^-- not an isotope

Pondering the potential implications of this form of thinking, it would mean there wouldn't be a form of invisibility that could beat a ^META operation:

>> z: ^ comment ["hi"] ~something~
== ~something~  ; isotope

>> z
== ~void~
   ; ^-- the COMMENT was seen by the ^META, instead of bypassed
   ; (today COMMENT's status as a purely invisible construct means you
   ; would get Z as a non-isotope ~something~)

Note that since parameters to functions are allowed to be meta if they need to be, changing a parameter from normal to meta would break commenting constructs. This is not too surprising, as if you have my-function comment "hi" 1 + 2 and change MY-FUNCTION's argument to be quoted instead of evaluated, that's another kind of parameter change that would break the commenting feature.

Meta parameters should be used very sparingly--far more sparingly than quoted parameters--so this may not be a problem. I notice that a function like RETURN (which takes its argument meta so it can return isotopes) could still chain an invisible function, as it would receive ~void~ as a measure of what an isotope was.

If Truly Pure Invisibility Is Not Implemented, Would People Just Reinvent it with Variadics in a Less Efficient Way Than The Current Evaluator Internals Do It?

Let's say someone writes:

foo: func [] [return comment "hi" 1 + 2]

Today this returns 3 due to comment's "purely invisible" status, RETURN takes its argument as a ^META parameter. If we decide meta parameters are allowed to see the "semipure" void isotopes described above, then it would just be like you'd written:

foo: func [] [return comment "hi"]

That would make it seem like COMMENT is unreliable. True, if you take a quoted argument you expect COMMENT to break in such cases as well...but meta parameters are evaluative and so it's a bit different.

Perhaps it should be just the ^ operator at the callsite that has the special vision, and meta parameters are evaluated and discard the void isotopes. This would mean you only have to learn the rule that ^ comment "hi" breaks the invisibility.

(Here we wind up with ^ either being a built-in thing the user has no way to write themselves, or invent a "supermeta" parameter flag to say "I can see void isotopes, too".)

Either way, "Impure Invisibility" Is MUCH Easier To Work With

The easy meta-transformations between ~void~ isotopes and plain ~void~ BAD-WORD! allow you to work with an invisible function gracefully...even when you don't know if it's invisible or not.

Early problems cropped up with pure invisible COMMENT like this:

>> f: make frame! :comment
>> f.discarded: "Ignore Me"

>> x: do f   y: 1 + 2
== 3

>> x
== 3  ; this could seem surprising, but maybe not?

But the new world does have at least an answer, meta your result and you can test for void cases and handle them as you wish.

>> x: ^ do f  y: 1 + 2
== 3

>> x
== ~void~

; alternately could have said `[^x]: do f  y: 1 + 2`

Yet it's still uncomfortable to imagine that an operation like DO could vanish, which is why DO tried to use void isotopes as a proxy for its return value. A ~void~ isotope could then be semantically interpreted or converted to pure invisibility on an as-needed basis.

"If You Don't Know What You're Doing, Then Do It Meta"

My urge to build safety into the system is driven by wanting to enable people to write generic code.

...but... trying to protect people from do f from vanishing when f is a FRAME! for the COMMENT function may be misguided. That protection could be breaking the very cool trick they are trying to perform.

And there's a real complexity cost to having a distinction between pure and impure invisibility. If you allow that distinction then impure invisibility has a meta form (a plain ~void~ BAD-WORD!) while pure invisibility doesn't have one. The powerful tool of META therefore offers no answers for a purely invisible function.

Maybe things like DO should have a switch to say do/vanishable and that's the right protection, otherwise they error if they could vanish. Same with unmeta?

Or maybe there's a better finesse, to say that the evaluator has some generalized protection which notices when you might have meant for an invisible result to be used, and there's generic mitigation for such cases. Like with x: do f y: 1 + 2, if it notices the potential for misunderstanding you can say x: vanishable do f y: 1 + 2...and you'd be provoked to decide if you wanted to use VANISHABLE or ^ based on what you were actually trying to accomplish.

That actually sounds like a really clever mitigation, if a word or symbol were picked for it!


To make a long story short: YES! I was on the right track...and this is the right answer.

UPARSE really made this clear. I believe it is the most sophisticated dialect ever put together in usermode, and it's very demanding. Its demands were much easier to meet when "pure invisibility" was dropped off the map.

This "new void" leverages much of the same mechanics that the old "pure" invisibility did. But its detectability via meta operations is foundational.

Branching statements that don't take their branch--and loops that never run their bodies--return the void isotope. And it vanishes:

>> if false [<a>]
== ~void~  ; isotope

>> 1000 + 20 if false [<a>]
== 1020

This would have created an obvious problem before, if you wanted to use IF with something like ELSE.

>> 1000 + 20 if false [<a>] then x -> [print [x]]
== 1020  ; old invisibility would behave like `1020 then x -> [print [x]]`

But the new trick is that THEN has a ^META argument for its left side parameter. That gives it the ability to see the invisible thing, as a ~void~ BAD-WORD! that reveals the existence of the so-called "~void~ isotope". It decides that means the left clause failed and it should not run:

>> 1000 + 20 if false [<a>] then x -> [print [x]]
== ~void~  ; isotope

The "Convenience" of Pure Invisibility Was An Illusion

Part of the argued benefit of pure invisibility would be that functions like ALL or other code wouldn't have to worry about the invisible things. They'd ask the evaluator to step, and it wouldn't pass back the invisible result...it would keep going until it had something non-invisible to report.

But that created a tangled web of behavior. Once the "pure invisibility" existed as a phenomenon, it meant dialects pretty much had to start caring about it. Otherwise they wouldn't be "first class" and seem lesser than the main evaluator.

The chimera that is the "void isotope" stepped in, to walk the line between existing and not-existing. And it's about as easy to handle as it can be. It's working well!