Making Invisible Functions (e.g. COMMENT, ELIDE)

The first idea of making constructs that would "vanish completely" leveraged a special kind of infix function, that would receive the entire evaluated value of the left hand side:

 comment: infix func [
     left [<evaluate-all> any-value!]
     right
 ][
     print ["COMMENT got left as" mold left]
     return left
 ]

 >> 1 + 2 + 3 comment [magic!]
 hi
 COMMENT got left as 6
 == 6  ; wow!

This was a workaround for the (seeming?) fundamental fact that you can't have such a thing as "invisible variables" or "invisible values". Certain functions just faked invisibility by repeating the previous value in the evaluator chain.

The possibilities seemed endless. For instance, imagine something like this:

case [
   conditionA [...code...]
   elide print "conditionA didn't succeed but running this"
   conditionB [...code...]
   conditionC [...code...]
]

To do that in Rebol2 or Red would be incredibly awkward. e.g. using a condition that runs code but evaluates to false, and then a throwaway block for the never-executed branch:

case [
   conditionA [...code...]
   (
       print "conditionA didn't succeed but running this"
       false
   ) [<unreachable>]
   conditionB [...code...]
   conditionC [...code...]
]

Similar awkwardness would arise in things like ANY and ALL, where you'd have to switch from using true and false based on which you were using...

any [conditionA (print "vanish" false) conditionB]
all [conditionA (print "vanish" true) conditionB]

Beyond being awkward, it simply can't work if what you want to vanish is the last expression. But ELIDE handled all these cases:

any [conditionA elide print "vanish" conditionB]
all [conditionA elide print "vanish" conditionB]
any [conditionA conditionB elide print "vanish"]
all [conditionA conditionB elide print "vanish"]

It Was A Neat Trick...But Problems Emerged

The trick of invisibility requiring a function to receive its left hand side meant a GROUP! or BLANK! (indicated usually by commas in lists) would break these constructs, as there was no access to a previous value:

 >> 1 + 2 + 3 (elide print "hi")
 hi
 ELIDE got left as null
 == \~null~\  ; antiform, not 6, d'oh!

 >> 1 + 2 + 3, elide print "hi"
 hi
 ELIDE got left as null
 == \~null~\  ; antiform, not 6, d'oh

Plus being infix forced the invisible functions to execute in the same step as whatever came before them, causing unsuspected results:

>> case [
        1 = 1 [print "branch"]
        elide print "reached here first :-("
        1 = 2 [fail "Unreachable"]
    ]
ELIDE got left as [print "branch"]
reached here first :-(
branch

There we see that when the evaluator visited the [print "branch"] block in the CASE it had to greedily run the ELIDE, which evaluates its argument and then yielded the code block as its result. CASE ran that code after the elide...out of order from what was desired.

Issues seemed to keep compounding. These invisible functions couldn't be reliably used with MAKE FRAME!, and people trying to simulate the evaluator's logic found it hard to detect and wrap them. That led to major issues with UPARSE trying to implement combinators that acted like ELIDE.

So the infix mechanism wasn't going to cut it. But it was too late: having been able to try out and develop all kinds of vanishing constructs convinced me of their value. I had to try another way...

Unstable Antiforms To The Rescue! :atom:

The seeming impossibility of having a "ghostly value" was addressed by making them unstable antiforms. They can't be stored in "ordinary" variables, but when returned from functions would have special treatment in the evaluator. Then there is a ^META domain in which they could be handled safely.

Terminology shuffled as the parts settled--this state was sometimes known as NIHIL, sometimes GHOST, sometimes "NOTHING"...

But ultimately, Antiform BLANK! Became VOID!

>> comment "Boo"
== \~\  ; antiform (void!)

Meta-variables can store them, and like everything else they can be LIFT'ed and UNLIFT'ed:

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

>> ^var
== \~\  ; antiform (void!)

>> lift ^var
== ~

>> unlift first [~]
== \~\  ; antiform (void!)

The concept of being able to pipe around and process "slippery" values in this meta domain (including trash states and other antiforms) wound up being wildly successful, and is the cornerstone of modern Ren-C.

Wide Variety Of Handling

Evaluators like EVAL and UPARSE specially preserve the last evaluative value in order to give the illusion of invisibility when ghosts are seen on the next step. Other constructs got to make a choice as to whether they wanted to embrace ghosts as part of the mechanic, or think of them as errors:

>> comment "comments return void"
== \~\  ; antiform (void!)

>> if comment "hi" [print "not tolerated in conditions"]
** Error: IF doesn't accept VOID! as its condition argument

>> all [comment "begin" 1 + 2 10 + 20 comment "end"]
== 30

>> any [comment "begin" 1 + 2 10 + 20 comment "end"]
== 3

e.g. for the above to work, ALL has to hang on to the last evaluated result as it goes...in case the next evaluated result is a void. This allows the 30 to fall out.

UPARSE stands as a great example of a system that has been able to build on meta-representation in order to be able to pipe around vanishing states using "special gloves" and build upon it to make new invisible behaviors...

>> parse "aaabbb" [collect some keep "a", elide some "b"]
== ["a" "a" "a"]

It's rather satisfying.

A Flexible Approach... But With Safety First!

Something that concerned me early on was that what had started as a narrow ability of just a few functions (like COMMENT and ELIDE) was becoming a case where generalized execution could possibly return ghosts, leading to unexpected results.

>> code: [comment "some arbitrary code block"]

; ... then much later ...

>> result: (mode: <reading> eval code)
== <reading>

>> result
== <reading>  ; oops, not what I meant!  wanted EVAL product in result!

The "safe" way to accomplish what you want here would involve LIFT and UNLIFT, to disarm the vanishing behavior of the ghost inside the multi-step evaluation of the GROUP! long enough to get it outside:

>> result: unlift (mode: <reading> lift eval code)
== \~\  ; antiform (void!)

But it's easy to forget to do that. In order to tackle the problem with a "safety first" mindset, the only time a multi-step evaluation will automatically erase a VOID! is if a function is defined as being VANISHABLE. Otherwise, you need to use a special identity operator (^ ). If you don't get that you'll wind up with a non-vanishing voidlike construct--an empty pack ("heavy void").

>> result: (mode: <reading> eval code)
== \~()~\  ; antiform (pack!) "heavy void"

>> result: (mode: <reading> ^ eval code)
== <reading>

The logic behind the heavy-voiding default is that quite often EMPTY PACK! and VOID! are interchangeable in meaning, and if not this cues you to use a LIFT-based solution or see if you really wanted arbitrary expressions to vanish.

Do Voids Bring Enough Benefits For Their Scariness?

No matter what way you slice it, an expression that can truly vaporize is something that can make you uneasy. Look at this CASE statement and imagine if FOO or BAR could vanish:

 case [
     foo [print "hi"]
     bar [print "bye"]
 ]

Sure... we can lament that if FOO is a vanishable function, it will wreck the geometry of the CASE completely. After dropping the FOO it will treat [print "hi"] as a condition and use BAR as a code branch.

But if FOO is a function that takes a BLOCK! as a parameter, it will also wreck the geometry of the CASE completely!

This is just the cost of doing business in the Rebol paradigm

Having to define functions as specifically being VANISHABLE for them to vanish without using the ^ operator makes them not particularly dangerous at all, in the scheme of things.

Just be judicious about making vanishing functions! Generally speaking it's better to just have a few... ELIDE is a generic vanishing function that can be understood and used at source level, which can save you making any other function surprisingly vanishable.

2 Likes