Should VOID Assigns Mean "Fully Remove A Key"?

Rebol2 and R3-Alpha distinguish between fields not in an object, and those that are in the object but unset:

rebol2>> obj: make object! [x: "unsetme"]

rebol2>> unset bind 'x obj

rebol2>> >> print mold obj
make object! [
    x: unset
]

rebol2>> unset? obj/x  ; asks if obj/x is an "UNSET! value"
== true

rebol2>> unset? obj/asdf
** Script Error: Invalid path value: asdf

Ren-C has this distinction today, as well.

>> obj: make object! [x: ~]

>> unset? $obj.x  ; asks if obj.x looks up to a TRASH! value
== ~okay~  ; anti

>> unset? $obj.asdf
** Error: Cannot pick asdf in obj

(Red says both obj/x and obj/asdf are "UNSET?", so you have to ask something like (unset? obj/asdf/jkl) before you get an error.)

This Is the "Unresolved" (vs. "Unset"?) Distinction

I think UNRESOLVED? is a pretty good name for the test saying there's no variable location to even find for something to ask about its state.

>> obj: make object! [x: ~]

>> unresolved? $obj.x
== ~null~  ; anti

>> unresolved? $obj.asdf
== ~okay~  ; anti

I don't know if the UNRESOLVED? question should accept multiple steps of not being resolved. Perhaps there's a refinement...

>> unresolved? $asdf.jkl.qwertyiop
** Error: Multi-step unresolved, use UNRESOLVED?:MULTI if intended

>> unresolved?:multi $asdf.jkl.qwertyiop
== ~okay~  ; anti

Beyond that, it's a little tricky to cover all the states related to "unsetness". There's TRIPWIRE! and TRASH! which both generate errors on reads, and currently both count as UNSET?. Then DEFAULT will overwrite null variables as well...so that's DEFAULTABLE? (void variables aren't really understood as to whether default should overwrite them).

I don't know. But I do think that terms like UNSPECIFIED?/UNRESOLVED?/UNDEFINED? all kind of cluster together... and among them UNRESOLVED? stands out to me as the best one to draw from for saying there really is no variable location at all.

Distinction Is Hazy For Things Like MAP!

Originally I had it so you would remove keys from MAP! by setting them to ~null~ antiforms.

Later I decided it would be more consistent if you asked to "remove" them by setting them to trash, so they'd be unset and you'd have to TRY to access them.

But removal from maps removes them from enumeration. This is like being "unresolved", not like being "unset".

A similar issue happened with the ENVIRONMENT! type for modeling environment variables. Removing with trash seems wrong.

Wild Thought: VOID (empty PACK!) To Mean REMOVE ?

Empty PACK! (now called "VOID") is an unstable antiform used to opt out of things like COMPOSE

But what if it was the way to ask for a removal from a MAP?

 >> m: to map! [a 10 b 20]

 >> m.a: void
 == ~[]~  ; anti

 >> m
 == #[map [b 20]]

This would only be usable on datatypes where removals were legal... such as MAP! and ENVIRONMENT!. You can't remove cells from a FRAME!--they're in fixed positions that have been compiled in. Today's OBJECT! doesn't allow removals either (though perhaps it could/should?)

The rule for accepting an unstable assignment of this form would be that this removes the item from enumeration entirely. Whereas if you assigned with a TRASH! the understanding is you're just making something that errors on access, but would still be in an enumeration of available keys.

Strangely Compelling Use Of Unstable Antiforms

This does feel like it pushes to a clearer resolution of my misgivings of the incorrect parallels between map/environment removals and trashing object/frame cells... in terms of the effects on enumeration.

1 Like

The concept of "void means remove" has gotten more entangled, now that it's legal to put unstable antiforms in metavariables (and, presumably, meta-map entries as well).

>> m: make map! []

>> m.^key: pack [1 2]
== \~['1 '2]~\  ; antiform

>> m.^key
== \~['1 '2]~\  ; antiform

This means mapping to the empty pack of VOID (and presumably to GHOST!, or ERROR!) should be legal.

That doesn't mean a non-^META assignment of VOID can't mean "remove". But it would be a point of "inconsistency".

Though is it any more inconsistent than saying ^e: fail "msg" is different from e: fail "msg" ... with the former storing an unstable ERROR! antiform, and the latter escalating the error to a panic?

Without some "assignment-based" mechanism to remove map keys, you'd have to use an operation that wasn't SET or a set-word. For instance, if UNSET were kept as a prefix operation:

unset $m.key

This requires branching your code in ways that aren't as pleasing as just being able to OPT your assignment out.

Rethinking "Wacky Packs"

I have proposed the wacky idea of using PACK! as a means of doing things like assignment-with-typechecking, using sub-band values (like a BLOCK! which is neither quoted nor quasi):

>> var: (typed [integer! tag!] 1020)
== \~[ [&[parameter! [integer! tag!]] '1020] ]~\  antiform

That could pretty clearly decay to the value minus the typecheck.

Then I floated similar ideas for unsetting things, using sub-band WORD! for example:

>> var: unset
== \~[ *unset* ]~\  antiform

That would also be not-quoted and not-quasi inside the pack...so it can be distinguished as a special signal. However, I wasn't sure what that should decay to (e.g. if you wrote any [x: unset ...])

But neither of these ideas would be effective for ^META assignment, because the meta assignment would just store the pack.

Why Be Any More Obtuse? Let VOID Unset Variables

I'm kind of feeling now like VOID in non-^META assignments should just unset variables. Then just live with the idea that ^META assignments can't be used to remove things.** You can't have it both ways.

Since VOID is an empty PACK!, this would be a change from the situation of getting a panic today when you try:

[x]: pack []   ; empty PACK! (definition of VOID)

Today the error guards you from misunderstandings, just as you're guarded against misunderstandings when you say:

[x y]: pack [1]

But the problem is, that if you go and start trying to invent workarounds for ways to "assign" the unset state (like ~[unset]~) that are any weirder than this, your weirdness creates new problems.


I think that I now believe that either VOID non-^META assignments should be the way that you get the unset state, or there just isn't an offered way to get the unset state through assignment.


Ergonomically, being able to get the unset state through assignment has a lot of advantages. And many assignments are not ^META.

I think it is worth it, to allow voids to unset variables--and remove them from maps--without having to write branching code to use a special removal operation. If you truly are writing something "full-band" that wants to do meta-assignment -or- unset the variable, I guess it's acceptable that then you have to branch your code to do either a meta-assignment or a non-meta void assignment (or use the UNSET function).

Meditation on True Unset

"Unsetness" is perpetually thorny.

The idea of true unset has been to create an out of band state which would defeat accesses by even meta variable access with ^VAR. You cannot specialize function arguments with the unset state... hence it's a state FRAME! variables can be in (present, but unset) but not one that's very friendly.

If FRAME! variables can be "present, but unset" what is it about MAP! keys that means they can't be present in an enumeration, but unset? Should object variables have this? Module variables?

If object variables can have the unset state, and you can get it just with a non-^META assignment to void, when would you write field: ~ vs. field: void ? Why prefer one over the other? At least one difference would be that ^field would work on the trash-assigned-variable, but not on the void-assigned-hence-unset-variable... but how does that inform things more generally?

I'll just throw in a little reminder of why trash is distinct from void in other places, e.g. why PRINT returns trash and not void. The reason is that VOID opts out of too many things., and:

 append data (... print "Hello"))

...should error, not be silent. TRASH!'s raison-d'etre is to throw wrenches in things, whereas VOID is silently accepted as an opt-out.

(So if anything, the current philosophy is backwards... VOID should silently unset a SET-WORD!, while TRASH should cause a panic on the assignment. Not that I believe it should, I'm just saying that if anything should be a panic, it's probably trash...not void.)

Back to the question: if VOID doesn't remove from MAP!, it's useless. But VOID can't remove fields from FRAME!, because you can't remove fields from frames--and it's not what we'd want it to do. You need to still be able to FOR-EACH enumerate a FRAME! to get at its keys, even if they're not assigned yet.

OBJECT! has the same underlying implementation as FRAME!--just an ordered set of slots in memory. Why would VOID assignment remove any of those slots from enumeration? LET variables start out life in the unset state...so why wouldn't a VOID assignment return them to that state?

It really seems like MAP! is just the odd one out here. But it's not storing variables, it's storing mappings. You can't bind into a map either. I guess we can say simply that the unset state is something only variables need to worry about preserving.

So Should You Declare "unassigned" Variables with field: ~ or With field: void?

:thinking:

TRASH! is now truthy, by design, so using VOID means if you declare things in ANY or ALL, that would be skipped as a no-op, at least in the current imagining of ALL, that throws out voids:

 eval wrap all [
     x: y: z: void  ; ALL throws out voids this would be no vote
     ...
 ]

But one problem with it is that it's not assigning void, so it's misleading. :frowning:

Reminder: Why Isn't VOID Was The Unset State?

Because several changes have come that are new, it's good to ask why Why isn't VOID as a Meta-Variable Assignment The "UNSET" state?

Where this starts to break is in FRAME! creation, where unspecialized slots look like they're assigned void:

>> f: make frame! subtract/
== &[frame! [
     ^value1: ~[]~
     ^value2: ~[]~
 ]]

The problem is, VOID is now used to opt-out of things. You couldn't then tell the difference between:

 make frame! [foo void]
 make frame! [foo]

This has been why the "truly unset" state has been important--something an evaluation cannot produce, yet a variable can represent (but never fetch with GET). I do note, though, that there's an advantage if void is allowed to set things to the unset state, that means there can be a difference:

&[frame! [
     value1: ~[]~  ; non-meta assignment (value1 is unset)
     ^value2: ~[]~  ; meta-assignment (value2 is void)
 ]]

Guidance Still Hazy, But Mechanics Look Solid

It bothers me some to have x: void be legal, because it looks like you've assigned void to the variable... when a non-^META assignment can't do that. This has been an error historically, and it feels misleading to make it a non-error.

However--as I've said--IF there is going to be a state that unsets variables through assignment, it doesn't make sense to use a state other than void (more generally, "missing pack slot"). There are too many questions opened up by using another state.

So as for the misleadingness... I could similarly say that it's misleading to do append [a b c] void and get back [a b c], "because you didn't really append void". No...because you can't append void to a block..but there is meaning applied to it anyway.

But when it comes to coherent source, this does make it seem better to assign things with ~ in general when you want to poison them. This works whether you're doing an assignment or a meta-assignment, and also has a smooth transition to if you want to use a labeled trash.

Given that type-checking is more strict now with respect to NULL and TRASH, this does give a possible reasoning for why you might want to use a "void-to-unset" assignment.

Right now if you have a variable which is meant to optionally hold a function value, you don't want to use NULL:

 some-hook-that-takes-integer: null   ; optionally a function

Because then, if you try to call it with no arguments when it's not set, you get a no-op in todays world:

>> some-hook-that-takes-integer 10
== 10

So you use TRASH! to "poison" such values, to stop that from happening.

(Alternative conceptions have been that maybe if something evaluates to null but is "not used" that raises an error--this idea of inert value discards being an error is something I think about from time to time, but have never actually tried. It would be easier now to try it, so I should just go ahead and see what happens.)

In any case, if you use ^some-hook-that-takes-integer you can get it back as a trash value vs. erroring on access. But TRASH! isn't easy to convert to void--it wasn't meant for it.

So if you wrote:

apply opt ^some-hook-that-takes-integer [10]

You would likely be intending "run the function if it's there, ignore it if it's not". But you'd be OPT-ing a trash, which doesn't give you void today.

However... if we were to go with the unset concept, and possibly leading colons turning them into nulls, that would possibly allow for:

apply opt :some-hook-that-takes-integer [10]

Then again, maybe OPT should be willing to turn TRASH! into null. :man_shrugging: Trash lets you use descriptive messages for why something is trash, which being unset would sacrifice.