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: ()]

>> voided? $obj.x
== ~okay~  ; anti

>> voided? $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 TRASH!, VOID, and NONE... all of which generate errors on reads. Is that UNSET? Then DEFAULT will overwrite null variables as well...so that's DEFAULTABLE?... :face_with_diagonal_mouth:

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.

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 (unstable antiform) To Mean REMOVE ?

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: ~
 == \~\  ; antiform (void!)

 >> 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! (assuming that's legal...) 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 VOID, NONE, or ERROR! should be legal... I think? :-/

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.

These ideas would be ineffective 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.

This would be a change from the situation of getting a panic today when you try:

[x]: pack []   ; empty PACK!

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

[x y]: pack [1]

But...maybe that's not a very interesting protection, and it's enough to just say y is unset.

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

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.

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 can use either TRASH! or void to "poison" such values, to stop that from happening.

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.

Setting the variable to VOID is a better bet:

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