Should ANY and ALL Vaporize "HEAVY VOID"?

Considering an issue from:

https://rebol.metaeducation.com/t/how-to-choose-between-returning-none-and-void/2171


There's some touchy-feely choices made in the constructs regarding VOID! (comma antiform) and HEAVY VOID (empty pack!).

I think it's important that neither of these "voidlike" values be truthy nor falsey.

>> if ^void [print "This is important."]
** Script Error: IF is missing its CONDITION argument

>> if (pack []) [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 VOID!, should it demand VOID! for opting out... and error on empty packs?

>> 1 + 2 while [null] [<b>]
== \~()~\  ; antiform (pack!) "heavy void"

>> all [if okay [<a>] while [null] [<b>] if okay [<c>]]
** Script Error: empty pack is not truthy or falsey

If it wasn't willing to erase empty packs, you'd need to use GHOSTLY or IDENTITY to erase the empty packs:

>> all [if okay [<a>] ghostly while [null] [<b>] if okay [<c>]]
== <c>

>> all [if okay [<a>] ^ while [null] [<b>] if okay [<c>]]
== <c>

It's probably the case that needing to intervene to stop the error is good. That's consistent with the protections of the evaluator in other instances to consider the heavy-voidification as a safety feature, to stop you from mistakenly thinking your value is coming from the last slot when it's not obvious at source level that it isn't.

Note that other constructs--like CASE--do not erase empty packs...only vanishable VOID!. 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 an empty pack unconditionally. And I think vanishable functions should be rare.

Vanishability is nice for debugging statements, because you can throw them in without disrupting the surrounding code

>> y: 10 + eval [x: 1 + 2, dump x]
x: 3
== 13

Because of this, there's been some pressure to say that PRINT be vanishable. So far, I've rejected that.

append [a b c] print "I think this should error"  ; rules out vanishable

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 vanishable operations, and if you want to erase something use ELIDE.

But ASSERT is vanishable, 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 vanishability.

Hopefully it's clear why I don't think there are that many applications for functions that return heavy void conditionally. GHOSTLY is a niche function that I don't think there are that many legitimate uses for.

And if you're going to come up with a construct that is conditionally vanishable, it should be discernible from the source-level syntax if it's a vanishing or non-vanishing invocation.

1 Like

So this instinct about "leaky ghosts" (e.g. functions that sometimes return ghost, and sometimes do not) has turned out to be ¡mucho importante! :taco:

I wrote about it in PARSE here:

https://rebol.metaeducation.com/t/leaky-ghosts-what-combinators-should-vanish/2437

But it's a pretty big deal in EVAL. You don't want the EVAL in situations like this to vanish:

 ^result: switch ... [
     ...
     ... [counter: counter + 1, eval code]
     ...
 ]

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 voids if you really intend it, and are conscious of what you're doing.

That's Now Done With The ^ Operator

foo: func [return: [void! integer!] x [integer!]] [
    if x > 1000 [return ()]
    return x
]

>> "some stuff" foo 304
== 304

>> "some stuff" foo 1020
== \~()~\  ; antiform (pack!) "heavy void"

>> "some stuff" ^ foo 1020
== "some stuff"

So ^ is actually modifying the function call machinery, saying "if this function isn't ghostable, don't do the normal switcheroo from ghost to empty pack".

(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.

This Allows VOID! Branches

>> "some stuff" if 10 < 20 [elide print "Oh my."] else [<else>]
Oh my.
== \~[]~\  ; antiform (pack!) "heavy void"

 >> "some stuff" if 10 > 20 [elide print "Oh my."] else [<else>]
== <else>

 >> "some stuff" ^ if 10 < 20 [elide print "Oh my."] else [<else>]
Oh my.
== "some stuff"

 >> "some stuff" ^ if 10 > 20 [elide print "Oh my."] else [<else>]
== <else>

And it means that eval [] actually returns VOID!... as it should. But EVAL would not be defined as "VANISHABLE" so you'd get a heavy void unless you used ^.

Applied To The Original ANY and ALL Question

>> all [when okay [<a>] while [null] [<b>] when okay [<c>]]
** Script Error: empty PACK! is not truthy or falsey

>> all [when okay [<a>] ^ while [null] [<b>] when okay [<c>]]
== \~\  ; antiform (void!)

The GHOSTLY operator is still useful, but it wouldn't come up quite as often... you'd need to have the "heavy-voidification" safety feature kick in from aggregate expressions to where it was too late for the ^ on the outer construct to help:

>> all [(print "for instance" opt null)]
for instance
** Script Error: empty PACK! is not truthy or falsey

>> all [ghostly (print "for instance" opt null)]
for instance
== \~\  ; antiform (void!)

Note also that OPT could be used to canonize HEAVY VOID back into VOID!s? Though that might accidentally erase NULL if you wanted null to be an error...

>> all [opt (print "for instance" opt null)]
for instance
== \~\  ; antiform (void!)