Why ^VOID vs. just VOID ?

I know VOID! is "the unstable antiform of COMMA!" :thinking: and these are frequently used as a way to "opt out" of things.

It's possible to make a VOID! out of a NULL, with OPT:

>> opt null
== \~\  ; antiform (void!)

>> append [a b c] opt null
== [a b c]

And it's possible to explicitly create one by evaluating a quasiform of a COMMA! (~, no comma in the rendering) to get the antiform:

>> ~
== \~\  ; antiform (void!)

>> append [a b c] ~
== [a b c]

But you get an error if you try to reference the VOID variable, unless you use a caret ("metaform"):

>> void
** PANIC: VOID is unstable antiform (VOID!), use ^VOID to access

>> ^void
== \~\  ; antiform (void!)

>> append [a b c] ^void
== [a b c]

This seems a bit ugly, compared to:

hypothetical>> void
== \~\  ; antiform (void!)

hypothetical>> append [a b c] void
== [a b c]

It's important to realize that the only way for a simple (non-^META) WORD! reference to give you an "unstable antiform" is if it looks up to an ACTION!, and runs it.

So if you want an arity-0 function that produces a VOID!, there are two predefined ones, and which one you pick depends on whether you want "vanishability" or not.

If you don't want vanishability, use NOOP

; noop: does []

>> noop
== \~\  ; antiform (void!)

>> 1000 + 20 noop
== \~()~\  ; antiform (pack!) "heavy void"

If you do want vanishability, use GHOST (arity-0 COMMENT)

; ghost: vanishable does []

>> ghost
== \~\  ; antiform (void!)

>> 300 + 4 ghost
== 304

Note that the distinction doesn't matter to <opt> parameters, which accept ANY-VOID? (~ and ~()~ antiforms) and NONE (the empty SPLICE! ~[]~):

>> append [a b c] noop
== [a b c]

>> append [a b c] ghost
== [a b c]

>> append [a b c] none  ; <-- though not a VOID!, probably best choice here
== [a b c]

It Would Be Misleading For VOID To Be A Function

Once ^META-references made it possible to store and read unstable antiforms directly, this meant you could assign the void state to arbitrary variables:

 ^var: ~
 void? ^var      ; "the var is void"
 any-void? ^var  ; "the var is also any-void" (but so is empty PACK...)

And if you were going to assign that variable to another variable, you'd have to use meta-fetch and meta-assign:

^other: ^var  ; set other to void, to match var

In a world where that's the case, a reasonable person would be led to conclude that you should write:

^foo: ^void

That seems like what a sophisticated programmer would think was "correct".

But if VOID were a function that synthesizes void--just for the prettiness factor of getting a void without needing to use a ^, that syntax would be broken.

>> ^void
== \~&[frame! []]~\  ; antiform (action!)

The idea that writing the correct-looking code would be broken sealed the deal that VOID should not be a function.

Fortunately there are two good-enough names for functions, that won't be confused with the type!

2 Likes

Note that ~ gives you a non-vanishing void:

>> 1 + 2 ~
== \~()~\  ; antiform (pack!) "heavy void"

There are good reasons for this (which tie into the reason that ^VOID is also non-vanishing):

>> 1 + 2 ^void
== \~()~\  ; antiform (pack!) "heavy void"

But if you want a "lighter than GHOST" way of making a vanishing void, you can use an empty GROUP!:

>> ()
== \~\  ; antiform (void!)

>> 1 + 2 ()
== 3

Some people might find the empty GROUP! pattern to be a pleasing option to use for optional parameters, since it has more "heft" than a simple tilde, but isn't as heavy as a WORD!:

>> replace [a b c b e] 'b ()
== [a c e]

>> replace [a b c b e] 'b ~
== [a c e]

>> replace [a b c b e] 'b noop
== [a c e]

>> replace [a b c b e] 'b ^void
== [a c e]

I'd imagine a lot of people would pick () if those were the only options. But () won't be caught by the "NODISCARD" logic. So you might want to be careful with that.

But replace just accepts voids if that's what your expression happens to synthesize. If you're passing something directly at a callsite, the best choice is probably an empty splice, e.g. a NONE

>> replace [a b c b e] 'b none
== [a c e]

This Use Of () Has Some Precedent In Historical Redbol

...though without antiforms it was always ambiguous whether you meant to put #[unset] into the lists or if you really meant "nothingness":

red>> append "abc" ()
== "abc"

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

red>> replace/all [a b c b e] b ()
*** Script Error: replace/all does not allow unset! for its value argument

Your daily reminder that isotopes and meta-representational coherence aren't some intellectual waste of time. :atom_symbol:

2 Likes

NOOP isn't terrible...but all things being equal, VOID is better. :slight_smile:

But good point about NONE (empty splice). NONE is truthy, too... which is pretty great:

append data any [
     something
     some other thing
     none
]

I may actually like that better than OPT ANY.

append data opt any [
     something
     some other thing
]

Assuming this has to be true, it raises the question of why is VOID! "unstable"?

It seems to be stable-ish, e.g. you can assign it with a plain SET-WORD:

>> ^x: ()
== \~\  ; antiform (void!)

>> x: ()
== \~\  ; antiform (void!)

If it's going to allow non-^META assignments, why is VOID! an unstable antiform?

I wouldn't be too sure. Look at your ANY case:

append data any [
     something
     some other thing
     none
]

What if you could write VOID there, just as a plain WORD!?

append data any [
     something
     some other thing
     void
]

You wouldn't get what you wanted...because ANY erases VOID (fortunately this would make a heavy void, and ANY would give you an error...because of the safety mechanism).

NOOP--and of course GHOST--don't look right in places you're planning to use the result, and that's a good thing.

I think ^void has the right "are you sure" aspect to it.


Reason #1

Instability is what makes VOID! unfriendly on WORD!-access.

If VOID! was not not unfriendly on WORD!-access, then that leaves the responsibility of being an "alarm" to TRASH!...

...and it's simply too convenient to "unset" variables with ~.

Reason #2

Instability is the characteristic that blocks testing with LOGICAL (or IF's condition parameter), which can't take VOID! or TRASH!. They don't make exceptions to prohibit these states, they only take stable values.

You also can't pass unstable antiforms to things like EQUAL?. Stability is just a general anchor for the system.

Consider if you write:

 some-func: func [x] [  ; not ^x
    probe x
 ]

I think it makes more sense to me if that errors when you say:

some-func ()

If we are willing to draw this line, then suddenly the implementer of SOME-FUNC has a lot of things they know they can do with X, e.g. if x [...] will never error on X (and X can never be an ACTION! that will consume your BLOCK!).

Your error locality is better... the VOID! is reported at the callsite for SOME-FUNC and not in the implementation of it.

The internals of the implementation check Stable* as a datatype, helping ensure coherence systemically.

It's just better.

I've run up against another ^WORD vs WORD issue, in the case of "Hot Potatoes".

For instance, VETO is implemented through a hot potato:

>> ~(veto)~
== \~(veto)~\  ; antiform (pack!) "hot potato"

The gimmick is that since it's a PACK! with an unlifted value in it, it can't be decayed normally. And so we can treat it as a lightweight signal that's error-like.

>> 1000 + 20 ~(veto)~ 300 + 4
** PANIC: Cannot decay hot potato PACK! ~(veto)~

Things that look for it can handle it specially, just as they could handle an ERROR! with a specific ID... it's just a much cheaper way of getting the same effect.

REDUCE and COMPOSE, but also PARSE groups, etc.

>> reduce [1000 + 20, if okay [~(veto)~], 300 + 4]
== \~null~\  ; antiform

Same Question Arises As For ^VOID, But...

It's pretty clearly nicer if you can just use the word VETO:

>> reduce [1000 + 20, if okay [^veto], 300 + 4]
== \~null~\  ; antiform

>> reduce [1000 + 20, if okay [veto], 300 + 4]
== \~null~\  ; antiform

But we also need to test for veto, and (veto? veto) has to work... so it would be a bummer if (veto? ^veto) failed because VETO was an ACTION!.

For VOID!s I did always wonder "is this rule really buying us anything"? And for hot potatoes, it seems it's buying even less.

The question we have to answer is if whatever protection we think we're getting from the rule is outweighing the damage we'd wind up doing trying to work around it.

Speaking completely generally, if you say x: y you don't have any particular guarantees... like "oh, I'm assigning one stable value to another stable value". Y could be a function.

If you write x: get $y and not x: get meta $y then this is a little different. You now explicitly know you're doing a non-^META get, and so we could say "therefore you know, you won't be getting any unstable antiforms back".

If we bent this rule for x: get $void and x: get $veto...how bad is it? And are they different?

There is a difference... in the sense that if you use a non-^META assignment, then the get $veto is producing a hot potato that is not assignable. Whereas non-^META assignment allows voids.

I kind of feel like that is different stakes. GET itself may return definitional failures (the erroring strategy for GET hasn't been completely worked through, but I think we'd be tying our hands if we didn't say that GET could return definitional errors to communicate information, at least on non-^META gets). But that might create some issues... if try get $veto suggested it wasn't able to get veto, vs. that it did and it was a hot potato...

The GET behavior and what WORD! does also doesn't have to be exactly the same...obviously they already aren't (a word runs a function, for instance).

It's definitely food for thought. I feel more uneasy about making void work vs. ^void than I do about saying that veto evaluates to the hot potato it contains. I definitely don't want to have to say (if condition [^veto]) and want to say (if condition [veto]). And I do like the idea that (veto? veto) and (veto? ^veto) both work.