UPDATE: After reflecting on the nature of the problem, I figured out that a rather targeted solution was possible. Simply make multi-step evaluations default to a mode that none-ifies voids unless they arise from a function or combinator that has been tagged as being "vanishable". Then, provide an operator to subvert the vanishing.
Targeted explanation here:
https://rebol.metaeducation.com/t/why-comment-vanishes-but-not-eval-of-comment/2563
Over time, new capabilities come online... new insights are formed... and it's good to go back to old designs and review them.
So let's start by asking: Is GHOST! Really Necessary?
What Is GHOST!'s Value Proposition?
While conceived for COMMENT, that doesn't actually get much use. The main value of GHOST! by far is ELIDE...something that has side effects and yet discards the result. In particular, its role in PARSE, but it's useful kind of everywhere.
>> parse "aaabbb" [some "a" elide some "b"]
== "a"
ASSERT is also very nice, to do in the middle of CASE statements, etc.:
case [
condition1 [some code]
condition2 [more code]
assert [something true if we got here]
condition3 [more code]
condition4 [more code]
]
I'm unwilling to give up the benefits of these "invisible" constructs for the sake of simplification. They're just too powerful.
Why Both HEAVY VOID (empty PACK!) and VOID (GHOST!)
It used to be that empty PACK! had GHOST!'s vanishing behavior.
It was fewer parts. But it did mean that a length-0 PACK! would break the pattern of what PACK! in general did.
>> "abc" pack [1 2]
== \~['1 '2]~\ ; antiform
>> "abc" pack [1]
== \~['1]~\ ; antiform
>> "abc" pack []
== "abc"
That isn't particularly pleasing.
Splitting the intents out allowed empty pack to fit better in with its family, despite being an alternative way of opting out where GHOST! can't mechanically work:
>> thing: null
>> opt thing
== \~,~\ ; antiform (ghost!) "void"
>> print "For instance..." opt thing
== \~[]~\ ; antiform (pack!) "heavy void"
>> append [a b c] (print "For instance..." opt thing)
For instance...
If empty packs vanished, it couldn't fill in this functionality gap.
Major Bummer: Vanishing Generalized Evaluations
The biggest problem with inventing a vanishing state is when it vanishes unexpectedly.
To recap this oft-talked-about point, imagine this fairly innocent code:
^result: (code: codemap.key, eval code)
Now imagine that eval code produces a GHOST!. You want that void to be stored in result, but instead it vanishes. So you end up assigning code to the result (since it's the result of code: codemap.key).
We could try and solve this with something like eval:voidable, such that if you don't provide the :GHOSTABLE refinement then EVAL either panics or provides a non-void placeholder value if it synthesizes a ghost.
>> eval []
== \~#void~\ ; antiform (some substitution, here a TRASH! rune)
>> eval:ghostable []
== \~,~\ ; antiform (ghost!) "void"
I don't like the EVAL:GHOSTABLE idea...
...because this is a general problem that affects any operation that returns ghosts sometimes... and I think it needs a general solution, not having to add a :GHOSTABLE refinement to every function that fits the pattern.
So my thinking would be that functions like EVAL would be written as true to themselves... returning a GHOST! if they meant ghost. But then if the type signature doesn't suggest "ghosts always", some distortion would happen. And you'd move the GHOSTABLE to the outside somehow.
This inversion winds up looking something like:
>> eval [comment "HI"]
== \~#void~\ ; antiform (trash!)
>> ghostable eval [comment "HI"]
== \~,~\ ; antiform (ghost!) "void"
-
While I used a TRASH! as a placeholder there to sort of "help hint at what was lost", it could probably be argued that the closeness of VOID and HEAVY VOID means you'd be better served by getting an empty PACK! substitution... it might not be as obvious what went wrong, but a lot of times it might not even make a difference.
- A flag could be stored on the HEAVY VOID to say it came into being as a VOID substitute, and reported in the console just as well as a trash could signal it to you.
-
I used a GHOSTABLE function, but this might not be able to be a normal function... because it has the exact properties of the function it's trying to "fix"... e.g. it returns GHOST! sometimes, but not always.
- We're running out of succinct non-WORD!-operators, but for the sake of argument I'll pick
^
- We're running out of succinct non-WORD!-operators, but for the sake of argument I'll pick
Taking on those adjustments:
>> eval [comment "HI"]
== \~[]~\ ; antiform (pack!) "heavy void"
>> ^ eval [comment "HI"]
== \~,~\ ; antiform (ghost!) "void"
Now let's look back at our "oblivious" situation, did it get helped? Seems you have two options now:
^result: (code: codemap.key, eval code)
^result: (code: codemap.key, ^ eval code)
Unfortunately, if you were trying to get a GHOST! into ^result, neither of these are going to help you. But at least the non-^ version had a shot at working (if HEAVY VOID was interchangeable with VOID for your purpose). And the code didn't do something bonkers by default.
We can imagine that the rules would be relaxed, if you rewrote your code as:
code: codemap.key
^result: eval code
The direct targeting of a ^META assignment could have an implicit ^ in the mechanic, to accept a GHOST!. But then, what about the overall expression?
(code: codemap.key, ^result: eval code)
Kind of back to the same question again, if setting propagates the value.
Would () Defaulting as HEAVY VOID Make Good Sense?
You could use the same operator to ask to remove the safeguard:
>> (comment "HI")
== \~[]~\ ; antiform (pack!) "heavy void"
>> ^ (comment "HI")
== \~,~\ ; antiform (ghost!) "void"
This would give parity in a world where the default disposition of eval [comment "hi"] was to give back a heavy void for safety:
>> eval []
== \~[]~\ ; antiform (pack!) "heavy void"
>> ^ eval []
== \~,~\ ; antiform (ghost!) "void"
We'd basically just be saying that a GROUP! kind of crosses the line--as a synonym for EVAL--into "some black box that doesn't always return a GHOST!"
But I don't very much like the idea that just adding parentheses introduces a variation in your ordinary evaluation. This needs thought.
![]()
UPDATE: Nope, there are two modes of evaluation... one for GROUP! and one for BLOCK!, and EVAL distinguishes them. It also doesn't let you EVAL:STEP a GROUP! (you have to consciously alias it as a BLOCK! if you want to step through it)...
What About Plain Old ^var When It Holds GHOST! ?
It seems it would probably be beholden to the same rules as function calls that may-or-may-not return a GHOST!.
Note that your standard "top-level" evaluation step in a multi-step expression isn't "ghostable". Contrast with--say--an argument to a function:
>> ghost? ^ghost
== \~okay~\ ; antiform
There's no reason to make a single-step evaluation to fulfill a function argument non-ghostable. Ghosts only vanish in multi-step evaluations (the vanishing happens in the "evaluator executor", not the "stepper executor").
So you could also do direct assignments:
^var: ^ghost ; transfers the ghost
But in a multi-step-operation which is accruing an evaluative product... if you didn't bless it with ^ it would turn into a void:
>> 1 + 2 ^ghost
== \~[]~\ ; antiform (pack!) "heavy void"
>> 1 + 2 ^ ^void
== 3
My instincts here is that VOID-TO-HEAVY-VOID safety is better than panicking.
(Note this is not "decay", it's an distinct evaluator mechanic that's built into the multi-step expression evaluator, e.g. EVAL as opposed to EVAL:STEP. This is also the point where it decides whether to promote an ERROR! to a panic, if the error wasn't consumed as an argument to anything.).
What About The ~,~ Quasiform Evaluating To GHOST! ?
I question the wisdom of making an exception for ~,~ to evaluate and act like a function that only returns GHOST!
Consider generated code like:
eval compose [some stuff (lift ^var)] ; and var happens to be a ghost
It's a fine line. If you had composed in code that was like comment "hi" that would be one thing, but synthesizing a lifted value from an arbitrary expression feels less specific.
Cases like this seem like they should have protection against vanishing, and you need to explicitly mark "vanishing okay" if vanishing is truly okay:
eval compose [some stuff ^ (lift ^var)] ; potential vanishing expected
But protection is not guaranteed if it's a function:
eval compose [some stuff (spread [comment "hi"])]
That compose'd-in COMMENT is allowed to vanish without needing a ^# to confirm it, because that's the baseline behavior we want from invisible functions.
This Looks Promising...
Hopefully it all makes sense, that "functions that only return GHOST!" get the vanishing exemption... and that exemption is not extended to ^META-fetches or ~,~ quasi-evaluations.
(The alternative would be to say you always have to use ^ to decorate things that vanish, functions included. At which point, we might as well just make ^ the vanish operator.)
Note it doesn't make a difference here if you are using a quasiform that makes void, or the product of a function that only returns voids... so you'd see this:
>> 1 + 2 ^var: comment "same behavior"
== \~[]~\ ; antiform (pack!) "heavy void"
>> ghost? ^var
== \~okay~\ ; antiform
>> 1 + 2 ^var: ~,~
== \~[]~\ ; antiform (pack!) "heavy void"
It does mean ghost would have a tendency to get dropped on the floor. But while making GHOST-returning functions and combinators should be easy, writing ghost-aware dialects (UPARSE or the evaluator itself) is going to involve at least a little extra thought. (...hopefully not too much, though!)