Should SET-WORD! Disallow Isotopic Assignments?

Historically, Rebol2 and R3-Alpha wouldn't let you assign UNSET! via a SET-WORD!, regardless of whether it was provided literally or through an evaluation. You had to use SET/ANY:

rebol2>> x: #[unset!]
** Script error: x: needs a value

rebol2>> x: print "hi"
** Script error: x: needs a value

rebol2>> set/any 'x #[unset!]

rebol2>> x
** Script Error: x has no value

At first, I thought Red made a concession in the direct assignment case:

red>> x: #[unset!]
== unset!

red>> x: print "hi"
*** Script Error: x: needs a value

However, that's not what's's an incompatible interpretation...where Red considers #[unset!] to be the datatype for unset!, and #[unset] (no exclamation point, illegal in Rebol2) is an actual unset value:

red>> type? #[unset!]
== datatype!

red>> type? #[unset]
== unset!

red>> x: #[unset]
*** Script Error: x: needs a value

But I bring it up because at one point in time, Ren-C actually had a specific behavior that it would only allow a SET-WORD! to assign an unset to a variable if it was not the product of a function call. Today this would look something like:

>> x: ~

>> x: print "hi"
** Error: Cannot assign evaluative isotopes to X

>> set/any 'x ~

>> x
** Error: X is ~ isotope (e.g. isotopic BLANK!, represents an unset state)

I've wondered if this rule strikes the right balance. If you really want to deal with isotopic values that are the result of an evaluation, you would use ^META, and get the non-isotopic (QUASI!) form. There'd be several ways of saying it:

>> x: ^ print "hi"
== ~

>> x: ^(print "hi")
== ~

>> x: meta print "hi"
== ~

>> [^x]: print "hi"
== ~

This makes much more sense now than when NULL and "unset" were the same state. And it would help keep isotopes under control.

Implications for the "Only Isotopic ACTION! Runs" Idea

This would mess with my current experimental branch where isotopic actions stored in variables are the WORD!-triggered kind (while regular actions would be inert).

It has FUNC returning isotopic actions, and you'd get an error:

>> foo: func [/refine] [...]
** Error: Cannot assign isotope to FOO with SET-WORD! (see SET/ANY)

Making an exception for action isotopes, and saying they could be assigned without an annotation really feels like it undermines the entire proposal and the safety you'd get from it.

I will note that I really did get a warm fuzzy feeling when the generators produced plain ACTION!, and something had to tip it into being isotopic. But I had to pan /foo: func [... /refine] [...] in no small part due to leading-slash being "taken" by refinements in function spec and apply.

This made me take a more serious look at the concept of retaking something like ->. Under the new rules that wouldn't have to finesse the assignment as well:

foo: -> func [/refine] [...]

foo: runs func [/refine] [...]  ; for those who dislike symbols

So either of these would act like set/any 'foo isotopic func [/refine] [...].

Wait... UNLESS... :thinking:

I just had a rather compelling thought, that such a tool could only accept isotopic ACTION!, and that this would provide natural guidance not to screw it up... because a plain assignment would error:

>> foo: func [/refine] [...]
** Error: Can't assign isotope ACTION! via SET-WORD!, use -> or INERT

So if all the function generators were committed to returning isotopes, it would be self-correcting. You wouldn't be able to forget to triage the assignment.

>> foo: inert func [x] [print ["x is" x]]
== #[action! [x]]

>> foo
== #[action! [x]]

>> foo: -> func [x] [print ["x is" x]]
== ~#[action! [x]]~  ; isotope

>> foo 10
x is 10

Now THAT is clever. You're forced into triage, and can't forget to do one or the other! Forgetting the annotation was one of the big Achilles heels of making generator products inert by default (hard to find bugs) but this ties that up.

Very promising premise! But there are tricky issues, like how today's METHOD needs to look back to quote what it's being assigned to, in order to know how to bind the value. Maybe it wouldn't need to... if -> also made that binding connection...and maybe there'd be no such thing as METHOD. :exploding_head:

Related Question: What About VOID ?

A corollary to this question is "Should SET-WORD! Stop Treating VOID Assignments as Unsetting Variables"

It might seem obvious to say "yes, that should be an error too". In fact...since I've vetoed the idea of variables actually being able to hold "voidness" it might seem like an even worse sin that should be prohibited... because you're not actually preserving the "integrity" of the assignment.

BUT... when I tried to rule it out I noticed that it was used in frames for things like setting refinements, like:

>> series [a b c]
>> value: 3

>> f: make frame! :append
>> f.series: series
>> f.value: value
>> f.dup: if integer? value [value]

>> do f
== [a b c 3 3 3]

But should the IF evaluate to void...the /DUP would not be set. That not-set state was leading the default behavior for the refinement being not set (to coerce it to NULL in the call).

This is an interesting use case, and it arguably isn't "doing an isotopic assignment" (void is not an isotope, and it has no QUASI! form). It's making a subtle judgment call, and it might be a good one. I'll keep this as-is for now.

Ultimate Goal: Freedom Of Choice :statue_of_liberty:

It's still definitely want to make it possible to override these behaviors if you find they get in your way. Certainly we'd need it for Redbol...but it should work at other granularities.

Why shouldn't you be able to say "hey, for just this function's body I want all the SET-WORD!s to assign isotopes without complaint"?


Just a thought ... if it is possible to use the annotation inside the generator on the return value, that would make it easy to build your own auto-annotating generators.

This would imply that things like FUNC and ADAPT would become enfix functions with a <skip>-able SET-WORD! or SET-TUPLE! on the left...and if they saw something there they could fold the behavior in for you... otherwise they'd just return the isotope.

Though if any code or branching separated the generator from the SET-XXX!, it wouldn't work. They'd even be foiled by just a GROUP!:

>> foo: func [...] [...]
== ~#[action! [...]]~  ; isotope

>> foo: (func [...] [...])
** Error: Can't assign isotope ACTION! via SET-WORD!, use -> or INERT 

So it would be possible, but kinda dodgy...making things more confusing.

It would probably better serve people who object to -> to have some module level setting that says "allow ACTION! isotope assignment without complaint".

Something kind of interesting comes up with the nature of a void assignment, which is that it's fundamentally different from other "isotopic assignments". Because there kind of "isn't any such thing as a void isotope":

>> (1 + 2, ~something~)
== ~something~  ; isotope

>> (1 + 2, ~)
== 3

So really, these put the evaluator in different situations:

>> 1 + 2 x: ~
== 3

>> 1 + 2 x: ~something~
; would be ~something~ isotope if we allowed it

I think it's pretty clear that assigning a variable voidness should not result in the variable being whatever was before it, e.g.

>> 1 + 2 x: ~
== 3

>> x
== 3  ; this would be nonsense.

So what's actually being assigned in an evaluator sense is staleness. And we can distinguish staleness, and tolerate it as something entirely different from trying to assign a "real isotope".

Another Thought: Isotopic Assignment Syntax

I had an idea for a syntax in SET-BLOCK!...including multi-returns...that would allow assigning isotopes.

>> x: spread [1 2 3]
** Error: Cannot assign isotope BLOCK! using SET-WORD!

>> [~x~]: spread [1 2 3]
== ~[1 2 3]~  isotope

A bit weird, yes. But it's less wordy than SET/ANY (I still don't know if I like /ANY as the name for that refinement):

>> set/any 'x spread [1 2 3]
== ~[1 2 3]~  ; isotope

And now that the QUASI! and isotopic representations are generic you can use this with GROUP!s:

>> [~(second [y x])~]: spread [1 2 3]
== ~[1 2 3]~  ; isotope

I really do think people should avoid putting isotopes in variables like this--it's a fringe requirement, by design. So if you're going to do it, I don't have a problem with it looking a bit dicey.

I definitely prefer it to other ideas like x.: spread [1 2 3], for a number of reasons--but the visibility of what you are doing is certainly one of those reasons.

This could also allow you to permit isotopes in things like FOR-EACH over OBJECT!s that had isotopic states.


So I think maybe this is another place isotopic objects can offer a solution (!)

Rather than try and hand back an isotopic action directly, a generator would make an isotopic object that had some kind of "assign" behavior. The SET-WORD! / SET-BLOCK! / SET-TUPLE! would coordinate with objects that implemented this method, allowing them to take over the process... subverting the standard rules of what would be legal just by default.

Then make isotopic action assignments illegal most of the rest of the time!

As rephrased in chat to @BlackATTR:

The working concept for isotopic actions is that they'd be the only kind that execute when they are attached to words. Then we make it difficult to get them into variables so you're pretty conscious when you have to worry about it. You won't be able to pick isotopic actions out of blocks/groups or put them into them because isotopes are illegal in arrays... and that will solve problems like block.1 running a function when you don't expect it to, so you don't need :block.1

But they could be in objects. So we'd need protection by saying something like for-each [key val] some-object [...] would not casually give you a VAL that was an isotope--it would error if it hit one.

  • You'd use for-each [key ^val] to get the meta form (if VAL was an action isotope in the object, it would become a QUASI-ACTION! in your enumeration, and not run from word references)

  • You'd use for-each [key ~val~] to get it isotopically (if VAL was an action isotope in the object, it would remain an action isotope in your loop body...but since you wrote ~val~ explicitly you pretty clearly know what you're setting yourself up for. So you won't be shocked by having to reference it by :VAL if you don't want it to run.)

The thorn in this scheme was that if isotopes are so unfriendly to put in variables, writing something: func [...] [...] gets awkward. You don't want every time you declare a function to have to say set/any 'something func [...] [...]

And having that done for you by a weird enfix operator like RUNS with something: runs func [...] [...] isn't just aesthetically annoying, it also has all the usual defeats of such things, e.g. something: (runs func [...] [...]) can't work because now RUNS can't quote left

Drum roll... :drum:

Use Isotopic Objects! So the ordinary isotopic action behavior with something like SET-WORD! would be to error. BUT you'd have function generators instead make an isotopic object that is able to coordinate with a method understood by SET-WORD! and friends when they go to assign, to say "all right, the isotopic object will take over performing the actual assignment". And it just quietly assigns the isotope without complaint.


The isotopic object idea would mean that anything which passes through those objects would be allowing you to get an assignable action back. So things like this would work:

foo: if condition [
    func [...] [...]
] else [
    lambda [...] [...]

You lose your safeguards the more of a black-box this becomes, e.g.

foo: do block

If isotopic objects are allowed to come back from DO (or whatever replaces it), and they rubber-stamp the assignment, then the ornery nature of action isotopes will do you no good here.

I'm doubtful there's any perfect answer, so being able to just stop actions-that-run-from-words from getting into those words accidentally 90% of the time would be good. Solving enumerations like FOR-EACH and not getting unintended evaluations is still the main place to bulletproof.


I do think this is a cool idea.

But trying to bulletproof things on the assignment-side here feels like a pretty serious uphill battle. I've posted about how the existence of the isotopes forces your hand in COMPOSE situations.

You can't put isotopes in blocks. So the list I present of the 3 main ways to triage an action isotope in a COMPOSE are:

  1. compose [x: (reify func [] [print "Hi", return 10])]
    produces [x: #[action! []]] ; plain action evaluates as invocation

  2. compose [x: (meta func [] [print "Hi", return 10])]
    produces [x: ~#[action! []]~] ; quasiform evaluates to isotope action

  3. compose [x: '(reify func [] [print "Hi", return 10])]
    produces [x: '#[action! []]] ; quoted evaluates by dropping quote to normal action

That gives you the three basic intents (running the function once and storing its result in X, storing a function isotope in X that will execute when you reference X, and storing a function value in X that will be fetched as a regular function value when you reference X).

But that's before adding in the spin that you can't assign action isotopes via normal SET-WORD!, but you need an isotopic object to step in and communicate with the SET-WORD! to suppress the error message.

That means the product in case [2] would be weirder, a ~#[object! [...]]~ with some voodoo inside.

Should ACTION! Isotopes Quit While They're Ahead?

I'm not quite at home with isotopic objects yet, so seeing the already new and weird notion of FUNC returning an isotope form...replaced by returning an isotopic even weirder.

An easy retreat from the weirdness is to just say that isotopic actions are allowed to assign via SET-WORD!, and leave it at that.

  • We're already getting a lot of protection coming from parameter types (not allowing isotopes by default, but being able to add them...see the writeup of how this was done in ARRAY/INITIAL).

  • I've pointed out above that trying to get things like DO to protect against giving back isotopes is sort of a losing battle, because it would just give back the object isotope from a black box...and you've got the same problem over again.

  • We already know there's not going to be a hard rule against isotopic assignments through SET-WORD!. ~true~ and ~false~ isotopes will be allowed as x: true and y: false would show...and I've basically proven why that shouldn't be done with some indirection through isotopic objects.

...or Am I Just Being Chicken? :chicken:

It's rather tough to feel this close to using the isotope distinction to protect assignments from becoming invokable functions too easily.

Making it hard runs up against the fact that people want foo: func [...] [...] to work, as well as foo: (func [...] [...]), and foo: do [func [...] [...]] etc. etc.

C++ has techniques that are similar to isotopic objects, where a function can create transitional classes that aren't really intended to be used as-is...but can be accepted by the constructors of other classes, so they can act polymorphically.

Because isotopic objects aren't meant to be stored in variables (in their isotopic state), we could get protection against a likely major source of accidents...with SELECT out of objects or a GET.

I think I'll have to try it. But there's a lot of impact to sort through even with the first draft (where isotopic assignments are allowed). So I'll sift through those bits first.

1 Like