When has Stopping VOID! (or old "UNSET!") Assignments Helped You?

I think it's good that VOID! is not conditionally true or false:

>> if print "This error seems good" [<does it not?>]
This error seems good
** Script Error: VOID! values are not conditionally true or false
** Where: if console
** Near: [... print "This error seems good" [<does it not?>] ~~]

And I think it's good that you don't get it accepted as a function argument by default...nor can you dereference a variable containing it by default.

But with NULL being used to unset variables, I kind of wonder:

>> value: print "how much do we gain by stopping this assignment?"
how much do we gain by stopping this assignment?
** Script Error: value: is VOID!
** Where: console
** Near: [value: print "how much do we gain by stopping this assignme...

For access, you have the ability to say :VALUE and get it. But for writing, you have to turn this into a SET/ANY. That's inconvenient.

Any time you call DO on CODE that is arbitrary, you have to switch away from the use of a SET-WORD!...

switch type of x: do code [
    void! [print "What a pain you can't get here without SET/ANY 'X DO CODE"]

How often has this actually helped anyone vs. just being a PITA?

I'm sure it's probably helped catch some mistake somewhere. But my memories of it doing so are few and far between, while my memories of having to use SET/ANY when I didn't want to are pretty hard to count.

The issue is that if we take this away, we take away a tool for error locality. Most notably things like:

 x: if 2 = 1 + 1 [select [a 10 b 20] 'c]  ; null gets VOID!-ified

You won't find out until you try to read X that you didn't get the value you meant, because NULL had to be reserved for the signal that the branch did not run.

This is all explained pretty well in "Why VOID! is not like UNSET! (and why its more ornery)". But quoting myself:

A void! is a means of giving a hot potato back that is a warning about something , but you don't want to force an error "in the moment"...in case the returned information wasn't going to be used anyway.

Is the assignment itself an important "hot potato" moment, or is that not one of the cases? Do we really need to know that the X: assignment didn't go well right then? What if the variable is never accessed (or if you have a if void? x: ...something... to handle it?

Looking for any anecdotes...

All I can think about is how annoying it is that I can't write generic code using X: and :X and trust the premise of symmetry and substitution. That's the main anecdote I have. I dislike what SET/ANY does to the elegance my generic code that is trying to work with any value...including VOID!. And in a language where safety is not the raison d'etre, seeing that elegance lost is frustrating.

Taking away this mechanism would take away the ability to stop a plain SET-WORD! assignment of anything...they would all be legal...including NULLs. But you could still stop it a lot of other places (function arguments by default, variable reads, and SET and GET without /ANY). With the objectives of the language being what they are, should we just go ahead and allow it?

Tangential thought: Maybe we can make [X]: have a different property, e.g. enforcing non-null values? You can't put nulls in blocks. So you'd have to say [X]: try null and get [_] as the result, vs. an error on [X]: null. Or maybe [X]: null sets X to VOID! and evaluates to a block containing a void value?

(This would be going with the idea that SET-BLOCK! was part of some multi-return value scheme instead of a pure synonym for SET of a BLOCK! as written today (which could be under another name, e.g. assign [a b] [1 2] giving a as 1 and b as 2. Then SET could mean something else entirely when applied to blocks!)

Continuing the general question of competitive analysis with JavaScript, let's look at what they do:

> function nothing() { return; }

> let x = nothing()
<- undefined

> if (x) { console.log("it doesn't error even on dereference"); }
<- undefined

> if (!x) { console.log("because it's falsey"); }
because it's falsey
<- undefined

That's even more forgiving. (I definitely like the neither-truthy-nor-falsey status of VOID!, and the falsey status of NULL, so those are unlikely to change.)

But does this point to the idea that it's better to make all assignments work, to get a solid "substitution principle"? e.g. wherever you could say:

  some-func (some expression)

You could alternately say:

 sub: (some expression)
 some-func :sub

Is the value of being able to take this for granted more than the value of having a datatype which defies SET-WORD! assignment, and requires special handling e.g. SET/ANY?

I really am leaning toward saying that prohibiting assignments of VOID! values via SET-WORD! is probably not all it's cracked up to be. Conscientious programmers should be trying things like:

 >> x: ensure integer! add 1 2
 == 3

That's even stronger, and if you said x: ensure integer! print "Hello" you'd catch the problem if you were so concerned. Or even x: add 1 2 matched integer! if you want to say it in another order.

Similar reasoning led to the allowance for NULL to unset variables. I feel like going all the way to saying SET-WORD! is SET/ANY, and GET-WORD! is GET/ANY, is probably what to do...and then let plain WORD!-access catch the unset variables and voids is the better answer.

1 Like

I'm still working to internalise the NULL/VOID/BLANK triumvirate, a part of that is current practice of the NULL -> VOID switcheroo (despite the fact I myself had suggested it somewhere—still working on it). Your suggestion here would present a more obvious way to un-set a word: x: void

I don't have any immediate thoughts as to why this'd be problematic.

On a tangential note: I'm not sure why PRINT returns VOID and wonder if it wouldn't be better to have it be silent like ELIDE? Would that create havoc if new users did confuse its usage?

Right. though I would call this "voiding" a variable, e.g. poisoning it in a way that will generate problems on casual access, if such access ever actually comes about.

To repeat what I've said: "variables can be set or unset, but values cannot. a variable that holds a value is set. a variable that holds no value is unset. mechanically, blocks can contain any values. null is a state reflecting the absence of a value and as such cannot be put in a block."

In the time over seeing VOID! act more and more like UNSET! did, I've questioned where it's worth it to change names of things if what you've wound up is kind of parallel to the old thing. But I really feel like the new concept of "set" and "unset" referring to a variable having no value (and not an ornery value which may appear in a block) is important. Not seeing that caused a lot of detours.

Think about something like making a frame that starts out empty, and then setting some of the values, and then executing the frame:

 >> f: make frame! :append

 >> f/series
 ; null

 >> f/value
 ; null

 >> f/dup
 ; null

 >> f/part
 ; null

 >> f/value: 10
 >> f/series: [a b c]
 >> f/dup: 3  ; in the post refinements-are-values world
 >> do f
 [a b c 10 10 10]

To me it seems like saying "at the beginning when you make a frame, none of the variables have been set yet". Then you "set" them, and then you can execute the frame. You don't want to start these variables out in a void state, because you don't want the /PART to cause it to error having that void value...which may be a legitimate thing for a refinement to be set to.

So I claim a variable that is unset is one that has no value at all. Variables can be unset, and values by definition cannot be unset. I float the idea of maybe saying a variable is UNDEFINED? if it is set to void...though I'm panning this as it's longer and less clear than VOIDED?, and too questionable in surface looks from UNSET?.

>> x: void

>> x
** Error: x is void and must use GET-WORD! vs. WORD! to access

>> type of :x
== @[void]  ; if recent proposal is used

>> void? :x
== #[true]

>> var: 'x
>> voided? var
== #[true]

What's changed here conceptually is that accessing a variable which is not set does not raise an error through word access. It just gives null... which is not a value, and value? null is false, and it cannot be a member of ANY-VALUE!...while VOID!s are. The errors on plain word access happen when you have a variable that is set, but set to a void! value.

The usefulness of the return result is that it isn't always VOID!, but NULL if nothing--not even a newline--was printed. This lets you use it with the likes of ELSE. It's also going with the traditional assumption that relying on the return result in any more general sense is probably a mistake. It's less convenient with ANY and ALL or similar, because you'd have to test for void? print [...] or not null? print [...] or value? print [...] kinds-of-things. But it still works.

(Performance dictates that returning the printed string itself is probably wasteful, since it is used so infrequently. It could arguably return a logic, so you could write did print [...] or not print [...] as well as use it with ELSE, but that bucks the traditional trend of thinking that being "ornery" in trying to use print's result is a good thing.)

Being able to elide print [...] covers the case where you want an invisible usage somewhere in the middle of an evaluation, which is nice and general...and not so strange once you get used to using it. Having dump routines like -- be invisible is nice though.