"Surprising" Ghosts: What Combinators Vanish?

The vanishing intent of GHOST! (antiform COMMA!) is now distinct from the empty intent of VOID (empty parameter pack, antiform BLOCK!).

>> 1 + 2 ghost
== 3

>> 1 + 2 void
== ~[]~  ; anti

This has brought to the forefront the question of which combinators should support vanishing.

Right Now, If <END> Matches, it Vanishes

>> parse [a b] [word! <end>]
** Error: PARSE mismatch

>> parse [a] [word! <end>]
== a

That's very useful.

Should TO and THRU--when parameterized with something that vanishes--also vanish?

>> parse [a #b #c] [word! to <end>]
== a

Seems pretty useful on the surface. But TO and THRU are intrinsically looping constructs...they iterate their rules. This means you could wind up with something that sometimes vanishes, and sometimes does not:

>> rule: [integer! | elide text!]

>> parse [a #b #c "hi"] [var: [word!, thru rule]]
== a  ; VAR got the product of WORD!

>> parse [a #b #c 1020] [var: [word!, thru rule]]
== 1020  ; VAR got the product of THRU RULE

That's a bit disorienting, how an elide managed to leak out. It's like the structure of the parse code isn't doing what you expect.

This is why the main evaluator's loops and branching constructs are not willing to vaporize when they stand alone. They're only willing to produce VOID. This keeps the basic structure of the code from picking up results you don't expect, unless you call something that specifically is known to have vanishing intent (and you can ask to convert voids to ghosts explicitly if you want).

Even invoking a rule BLOCK! itself--if you think of rule invocation as like PARSE's version of calling a lambda--raises some questions about "surprising" ghosts:

>> rule: [integer! | elide text!]

>> parse [a #b #c "hi"] [var: [word!, rule]]
== a  ; VAR got the product of WORD!

>> parse [a #b #c 1020] [var: [word!, rule]]
== 1020  ; VAR got the product of RULE

Unfortunately PARSE doesn't have the analogue of the GROUP! vs. BLOCK! distinction for code, where one can be transparent and the other "surprising". [elide some [rule1 | rule2]] can be genuinely useful as a source grouping.

On That Note, Should LAMBDA Be Willing To Vanish?

At the moment, you get VOID and not GHOST from lambdas whose bodies vanish.

 >> test: lambda [] [comment "this is a test"]

 >> 1 + 2 test
 == ~[]~  ; anti (void)

It's hence impossible for a LAMBDA to produce a GHOST!... you have to use functions with a return value. But does that make sense?

I think I'm willing to say that lambdas and rule blocks can vanish. They probably have to.

But I'm just not 100% on board with the idea of this vanishing leaking out through other constructs. It seems likely that you'd start getting "vanishing sometimes" behavior on accident.

So while it may seem nice if you're just looking at the specific case of to <end> vanishing, I think the long game favors saying that it's easy enough to write elide to <end> if you want.

Having plain <end> in isolation vanish is fine, because that's predictable and happens every time you use <end>. But it wouldn't be so with vanishing every time you use TO... hence the problem with it.

So I made a function called UNGHOST to help with this:

>> 1 + 2 comment "HI"
== 3

>> 1 + 2 unghost comment "HI"
== ~[]~

>> 1 + 2 unghost 10 + 20
== 30

But then I noticed that most all of the combinators in UPARSE (that call subparsers) would have to end with return unghost ^result instead of just return result.

It feels redundant. Because the pattern is: does the return spec have GHOST! in it, and if not, run UNGHOST. So it might not hurt if FUNCTIONs which don't have GHOST! in their RETURN: spec would automatically convert ghosts to VOID.

I could make this something that just COMBINATORs do. But because of the systemic "ghost suppression" bias I am feeling, I think it would be good for the health of the system overall.

Just Coerce To Void For RETURN, Or Arguments Too?

While it seems helpful or at least benign for RETURN, I'm not sure if it's completely wise to make this a general rule for type checking... that anything which would take a VOID would be willing to accept a GHOST! converted to VOID:

>> append [a b c] comment "is this bad?"
== [a b c]

>> append [a b c] ()
== [a b c]

It's not obviously terrible. But it does kind of take off some guardrails, to where you might not be saying what you think you meant to say.

There's no immediate advantage that I can see, and it just seems to promote accidents. So I think I'll limit it to return type coercion.

We Can Get The Best Of Both Worlds

So the concept that I am putting forth is that we treat functions differently based on their type signature.

  • If a function returns ghosts always (an "unsurprising" ghost) then let them vanish.

  • If a function returns ghosts sometimes (a "surprising" ghost), then still produce a ghost... but don't vanish.

At first I thought that ^ would be an operator for overriding the non-vanishing behavior, transforming surprising ghosts into unsurprising ones (humorously called UNAFRAID).

>> parse [a #b #c] [word! to <end>]
== ~[]~  ; anti (void)

>> parse [a #b #c] [word! ^ to <end>]
== a

However, the much more useful application of that scarce operator is to approve surprising actions. This is when ACTION! is returned by operations that don't always return actions, which will panic on assignment by default. We don't want one operator to quell both surprises: that would mean people who simply meant to approve actions could end up wrecking the structure of their code on ghost appearances.

So if there's an UNAFRAID token, it will have to be something besides ^

Key To Observe: UNAFRAID Can't Be a Combinator

What UNAFRAID has to be is like | and ||... something the BLOCK! combinator specifically recognizes as it processes.

Unfortunately I feel like we're kind of out of symbols. We do have the option of ' making a resurrected appearance, but I don't think it's a good idea.

1 Like