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 Are VOID (empty PACK!) and GHOST! Distinct?
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 VOID to fit better in with its family, and solidly serve its role as the "opting out":
>> thing: null
>> opt thing
== \~[]~\ ; antiform (pack!, void)
>> append [a b c] (print "For instance..." opt thing)
For instance...
== [a b c]
If VOID vanished, it couldn't do things like that.
Slightly Lame: ()
Has Been GHOST! and not VOID
Evaluating an empty BLOCK! or GROUP! has been giving you a GHOST!, and not a VOID:
>> ghost? eval []
== \~okay~\ ; antiform
>> ghost? ()
== \~okay~\ ; antiform
It would be rather neat if ()
produced VOID, especially because synthesizing a void right now requires you to either say ~[]~
or ^void
>> replace [foo baz foo bar foo] 'foo ^void
== [baz bar]
The empty group would be slicker, and fairly semiotic:
>> replace [foo baz foo bar foo] 'foo ()
== [baz bar]
Given new rules about non-^META void assignments, it would also provide a cool way to unset a variable:
var: ()
But under today's mechanics, ()
has to be GHOST!...not VOID...given that (comment "hi")
is GHOST!. It's the "translucent" initial state.
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 ghost 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:ghostable
, such that if you don't provide the :GHOSTABLE refinement then EVAL either panics or provides a non-ghost placeholder value if it synthesizes a ghost.
>> eval []
== \~#ghost~\ ; antiform (some substitution, here a TRASH! rune)
>> eval:ghostable []
== \~,~\ ; antiform (ghost!)
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"]
== \~#ghost~\ ; antiform
>> ghostable eval [comment "HI"]
== \~,~\ ; antiform (ghost!)
-
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 GHOST! means you'd be better served by getting a void 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 void to say it was a ghost 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!) "void"
>> ^# eval [comment "HI"]
== \~,~\ ; antiform (ghost!)
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)
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 VOID was interchangeable with GHOST! 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.
In Such A World, () Defaulting as VOID Makes Good Sense
You could use the same operator to ask to remove the safeguard:
>> (comment "HI")
== \~[]~\ ; antiform (pack!) void"
>> ^# (comment "HI")
== \~,~\ ; antiform (ghost!)
This would give parity in a world where the default disposition of eval [comment "hi"]
was to give back a void for safety:
>> eval []
== \~[]~\ ; antiform (pack!) "void"
>> ^# eval []
== \~,~\ ; antiform (ghost!)
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!"
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!, void)
>> 1 + 2 ^# ^ghost
== 3
My instincts here is that ghost-to-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, Especially ()
As VOID
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 ghost, or the product of a function that only returns ghosts... so you'd see this:
>> 1 + 2 ^var: comment "same behavior"
== \~[]~\ ; antiform (pack!, "void")
>> ghost? ^var
== \~okay~\ ; antiform
>> 1 + 2 ^var: ~,~
== \~[]~\ ; antiform (pack!, "void")
>> ghost? ^var
== \~okay~\ ; antiform
It does mean ghosts would have a tendency to get dropped on the floor. But while making GHOST!-returning functions and combinators should be easy, writing ghost-aware code (UPARSE or the evaluator itself) is going to involve some special skills.