Should ANY-VALUE! Include Isotopes?

So if you don't put any type restrictions on a parameter, it will allow isotopes:

 >> foo: func [x] [probe x]

>> foo null
~null~  ; isotope

And right now, that is a synonym for ANY-VALUE!.

 >> foo: func [x [any-value!]] [probe x]

>> foo null
~null~  ; isotope

But what if you want to exclude isotopes? We could have a name for the typeset excluding isotopes (perhaps ANY-CELL!). Or we could say that ANY-VALUE! doesn't include isotopes by default, and so you have to write [any-value! isotope!] if you want them... or just include the specific ones you intend (like [null! any-value!].

Right now I'm going the route of saying ANY-CELL! is the narrower set of values that do not include isotopes. Is there a better term for saying something can be put into an array vs. not?

"10 is an integer! which is legal to use as a cell, while ~null~ isotopes are not legal as cells, but can be variable values"

It seems shorter and cleaner than ANY-REIFIED! or something like that. I don't know any better options.

1 Like

Let me point out that it seems best if typechecking applies to the unescaped form. So if you have a ^META argument which says it can accept a something unstable like a PACK!, that is written as func [^arg [integer! pack!]], not func [^arg [quoted-integer! quasi-pack!]].

As such, isotope! would have a different meaning for meta and non-meta parameters. This suggests needing a distinction between stable-isotope! and any-isotope! :frowning:

This raises a question about the meaning of func [^arg [any-value!]]. Does that encompass unstable isotopes as well as stable ones?

A term that could subsume stable isotopes and array elements could be ANY-STABLE!, which would then leave ANY-VALUE! exclusively for things you can put in an array. But I think this runs against the natural usage of the term "value"... if you ask "what is the value of the variable" and something like a ~true~ isotope is legitimate for a variable to hold, that's the variable's value. I'd also say an object contains "keys" and "values".

But since a variable cannot hold things like a PACK! (block isotope) or a RAISED! (error isotope), it is probably best to avoid saying they are "values". They're unstable isotopes. What happens to an unstable isotope is that if they're not in the type checking of a ^META parameter, they decay... e.g. a pack will decay to its first item, and a raised will decay to causing an "untrappable" failure (exempting special functions like sys.util.rescue).

This points to the idea that if you want to take only things that go in BLOCK!s/GROUP!s, those need a name. At the instant of writing I've called those ANY-CELL!

Implementation Naming Issue: Cell Datatype is Superset

In the mechanical implementation itself, I used the name Cell as the base class of Value... not vice versa. In OOP terms, this suggests all values are cells...but not all cells are values.

The distinction this was intended to draw was the difference between something that has all the information needed for binding (e.g. a variable's value) and a raw array element that can be imaged multiple times and places with different bindings.

This gives you partial type coverage--as a routine for writing an isotope can take the address of a Value, which prevents accidentally writing them into a block! or group!. While writing something like an integer! can take the address of a Cell and work for either. However, reading routines like testing to see if a value contains an isotope takes a cell.

So the naming doesn't line up with ANY-CELL! meaning ANY-ARRAY-ITEM!. I don't feel like going into the code and giving another name to the base class, like Element. And I don't like the idea of the implementation using the word "cell" to mean something different than what a user of the language would say it means.

Perhaps Element is a good name for things you can put in array slots, retaining Cell for the universal base class that applies to Element and Value?

I haven't needed in the implementation to specifically call out "Elements" distinctly from "Cells". If I used my imagination I could probably think up some restrictions...for instance that you can't do pointer math on Cells without casting them to Element. But really the core issue in the implementation has been that you are restricted from doing variable lookup on cells without combining them with "specifiers" to make "values".

I think I'm happier with ANY-ELEMENT! or ELEMENT!. This suggests maybe ANY-CELL! or CELL! could--in compliance with the implementation--subsume unstable isotopes.

I'm scratching my head over the usefulness of an Element subclass in the implementation itself (beyond the exposed typeset). One of the problems is that Rebol guts are based on using common array routines for objects and function frames...and so even common-sense-sounding ideas like "you can't just increment a pointer to a variable to get to another variable" are subverted all over the place.

I'll have to keep thinking on it, but at least there's a bit of progress with the "element" term. So append would accept [void! element! splice!]... with void resulting in a no-op, element adding a single item, and splice adding an array itemwise.

1 Like

This wasn't always the case...

Originally, ANY-VALUE! did not include NULL

Typesets were implemented as bitsets based on the cell type byte. Nulls had a distinguished type byte of zero. Then the corresponding bit for null was not set in ANY-VALUE!.

So an ANY-VALUE! was considered everything but a null. If you wanted a function to be able to take a null argument, you'd have to say [<opt> any-value!]

Back then I claimed that NULL was a "non-valued state", with the terminology that "it's not a value, because you can't put it in a block".

Nevertheless, the variables at the time could hold nulls. This runs up against my new competing terminological idea: "values are anything you can put into variables."

Excluding Isotopes By Default Seems Appealing

When users reach for a type constraint to say "I purposefully mean to accept many values", it's nice if isotopes aren't included in that.

NULL exclusion is a good one, with a historical basis. Much of the reason for existence of NULL is that it's rejected by most functions (outside of things like IF which are deliberately testing for nulls). Functions generally shouldn't take them.

Another big-new-cool reason is that it would exclude "ACTIONs". If you can't get the activated form of an action by default, your code doesn't have to be paranoid about putting in GET-WORD!s everywhere to disable them.

(NONEs are already excluded from normal parameters now, you have to use a ^META parameter to get them.)

Excluding isotopes would exclude things like logic ~true~ and ~false~, along with splices and other things. I don't have a problem personally with routines that care about logic needing to explicitly add LOGIC? to the type constraints.

The Code Sticks Us With Value*'s Meaning

All things being equal, I might say that "variables can hold values -or- stable isotopes (or voids)" would be good for being able to say ANY-VALUE? instead of ANY-ELEMENT?

But the code... has a meaning for Value*, and it really is pretty much literally "the kind of state that can be held by a variable".

The implication here is that all the places that say [<opt> any-value?] right now are redundant...that ANY-VALUE? includes null, and if you want to exclude nulls you have to use ANY-ELEMENT? and add in the isotopes you want from there.

Biggest Cold-Feet Issue: ACTION Isotopes By Default

I started reviewing this point when I was right on the brink of turning all the [<opt> any-value?] into [any-value?] and just moving along...saying what's done is done.

But this means you can be receiving something that will execute when you reference it by a common default.

A bit part of me wants to say that functions won't accept frame isotopes (e.g. actions) and you have to ^META them, the same way you have to do with void isotopes representing unset values.

But features break. COLLECT is implemented by passing the block you give it to a function with an argument KEEP, and passes the keeper function as that parameter. The function builder takes care of the binding. I also use the feature in UPARSE, where the incoming PARSERs are live actions as well as parameters.

Some comfort can be taken that at least this is a place where you have a type block to enforce the policy of your choosing. What worried me more about things like code enumerating blocks is you didn't get that choice.

Long Story Being Short: ANY-VALUE? Includes Isotopes

It does make me wonder, though, if a different parameter convention for accepting "live" actions is needed. GET-WORD! was once proposed as a good visual cue:

 foo: func [value [any-value?]] [...]   ; would reject ACTIONs despite ANY-VALUE?
 bar: func [:value [any-value?]] [...]  ; accepts the live action

This would attack the problem without saying that a live action wasn't an ANY-VALUE... it would just be prohibited by some other rule (much like how NONE is prohibited for another reason contextual to parameters).

But that's a separate design question, and for now the definition stands:

A "Value" is any state that can be stored in a variable.

1 Like