Ah-ha… I’d completely missed this use of void, as a return value.
Though this does now raise the question of whether a function should return VOID or GHOST. Both seem to vaporise in lists, after all. I guess it depends on whether you want the function call to be ignored or not.
There's some touchy-feely choices made in the constructs regarding voids and ghosts.
I think it's important that they're not truthy or falsey.
>> if void [print "This is important."]
** Script Error: IF doesn't accept VOID for its CONDITION argument
>> if ghost [print "This too."]
** Script Error: IF is missing its CONDITION argument
However, ALL is kind of like a chain of IF statements testing for logic. So given the existence of GHOST, should it demand GHOST for opting out... and error on VOIDs?
>> while [null] [<b>]
== ~[]~ ; anti (void, never ran loop body)
>> all [if okay [<a>] while [null] [<b>] if okay [<c>]]
** Script Error: Invalid use of void
If it wasn't willing to erase voids, you'd need to use ELIDE-IF-VOID to erase the voids:
>> all [if okay [<a>] elide-if-void while [null] [<b>] if okay [<c>]]
== <c>
But I feel like it works out better in practice if ANY and ALL consider void as no vote. Also, if all the expressions opt out of voting, the ANY or ALL overall is void.
>> any [void, while [null] [<b>], void]
== ~[]~ ; anti
But other constructs--like CASE--do not erase VOID...only GHOST. They have a "structure" to them, and it's desirable to not disrupt that structure too easily.
I think it should be a very rare choice to make things return VOID unconditionally.
And I think it should be an even rarer choice to return GHOST (e.g. because of the damage it can do to things like CASE or SWITCH structure). And when GHOST is returned it should almost always be unconditional.
GHOST is nice for debugging statements, because you can throw them in without disrupting the surrounding code
append [a b c] print "I think this should error" ; rules out void
Also, though people reach for PRINT for debug output, I think it's poor for that. The fact that it evaluates blocks means I don't like the idea of print x taking too many kinds of input, that could one day become a block and surprise you by evaluating. So it's already a bad generic "debug dump". And logging with the same thing you use for committed output makes it hard to search for debug code and remove or disable it.
I like there being relatively few GHOST-returning operations, and if you want to erase something use ELIDE.
But ASSERT returns GHOST and that's neat in things like CASE or SWITCH for asserting something as true when you've gotten to a certain point:
case:all [ ; ALL -> don't stop on first condition matched
x < 10 [y: <lesser>]
x > 10 [y: <greater>]
assert [(x = 10) or (find [<lesser> <greater>] y)]
x = 10 [print "You can imagine this kind of thing being useful"]
]
I should go through and replace these with real useful examples someday, but my hope is people get the point abstractly.
Anyway, I think it's nice to be able to do that particular thing without saying ELIDE ASSERT. A few other constructs make the cut of justifying GHOST.
Hopefully it's clear why I don't think there are that many applications for functions that return GHOST conditionally. ELIDE-IF-VOID is a very niche function that I don't think there are that many legitimate uses for. If you're going to come up with a construct that does return GHOST conditionally, it should be discernible from the source-level syntax if it's a vanishing or non-vanishing invocation.
If EVAL is allowed to vanish, then you could wind up assigning the calculation (counter + 1) to result, and this is surprising.
But you need some way of evaluating and being able to get ghosts if you really intend it, and are conscious of what you're doing.
So far that's been with eval:ghostable.
(I think eval:ghost or eval:ghosts doesn't communicate as well that what it means. eval:ghostly is shorter but also less clear. eval:transparent is longer than ghostable and also just kind of less informative.)
I Wish This Could Be Pushed Outside...
It would be technically possible to rig something up where the evaluator would be complicit, in saying that if a function's signature wasn't such that it always returns GHOST!, that it would turn the ghost into a VOID unless a special participatory tool was used.
We talked about ^ being free. Not that it's a great choice, but there would be worse ones.
Let's imagine that it represented this intention:
foo: func [return: [ghost! integer!] x [integer!]] [
if x > 1000 [return ghost]
return x
]
>> "some stuff" foo 304
== 304
>> "some stuff" foo 1020
== ~[]~ ; anti (void)
>> "some stuff" ^ foo 1020
== "some stuff"
So it's actually modifying the function call machinery, saying "if this function doesn't always return ghosts, don't do the normal switcheroo to void".
I know this isn't necessarily super obvious. But what @ does is a quirk of the system as well, applied to solve tricky fundamental problems that can't be done other ways.
This Is No Small Issue, And Worth It To Solve
It comes up a lot of other places, e.g. UNLIFT... which takes in a quoted or quasiform and drops it a level. Should that vanish by default, or do you need yet another UNLIFT:GHOSTABLE?
There's an issue with ^var vanishing and causing surprises as well. I don't think we want to be writing [^ ^var], so something else needs to be done.
One small step could be to say that ^(var) is willing to vanish, while ^var is not. That might not seem to make a hell of a lot of sense, but GROUP! evaluations are typically allowed to vanish, which is why you can put COMMENT and ELIDE and such inside groups.
>> x: eval [10 + 20 (comment "this works")]
== 30
This would cut down on the number of places that the mistake can happen.
Think I Need To Give This a Shot
I'm not happy with distinguishing EVAL vs. EVAL:GHOSTABLE, and that :GHOSTABLE is just one of those refinements that starts spreading everywhere. Better to have a uniform solution.
As we discussed, ^ is kind of an outlier where you're like "uh, what should that mean?" Having it do something genuinely useful is better than trying to weasel together an argument for why what it does "makes sense".
Having to say ^(var) to get vanishing, with ^var giving ghosts as void, is imperfect but better. You'd basically never write ^(var) if you could just say ^var so it makes sense to encode the intent.
If you're super-conscious from experience that ^(...) can vanish, then when you get ^(my expression) situations you don't want to vanish, you can mitigate that by deghosting.