Which isotopes are currently in use?

Since generalised isotopes, every type has an isotopic form which cannot be stored in blocks. But not every isotopic type makes sense — for instance, what could one do with an isotopic TIME!? So it seems that, out of necessity, practical uses for isotopic values are decided on an ad-hoc basis.

So far, the ones I know about are these:

  • Isotopic GROUP!s are splices, which are pretty trivial to understand
  • Isotopic FRAME!s are actions, which run themselves when invoked (though in that case I don’t quite get what non-isotopic FRAME!s would be)
  • Isotopic VOID! is an unset variable, I believe, though the naming seems to have changed so often that I’ve had a lot of trouble following the relevant forum posts
  • Isotopic COMMA! is… something… which acts as an expression barrier somehow (though similarly to FRAME! I have trouble distinguishing this from the non-isotopic COMMA!)
  • Isotopic WORD!s seem to be error states of some description, as well as booleans — except that makes no sense to me, since surely booleans should be storable in blocks‽
  • I’ve seen a few references to an isotopic NIHIL, but I don’t know what that does nor what a non-isotopic NIHIL would be

So… clearly there’s a lot missing. Do we have any comprehensive list of which of the various isotopic types are currently in use, and what they are used for? If not, it would be awfully convenient to have one…

At the moment, not all isotopic states have meanings.

It is likely the case that you should be disallowed from creating ones that don't have meanings, to reserve them for future use.

(For lack of a better name these are called ISOWORDs for now.)

I wrote about the boolean design in a separate post.

It may be that these are narrowed to only a small set of choices reserved by the system like ~null~, ~true~, and ~false~... and if you want a generic error-trigger (like a labeled unset variable) that would be something else, like an isotopic tag.

Yes, isotopic VOID is now called TRASH, and used for unset variables.

(If you find posts with outdated naming let me know--just leave a reply citing the problem and I will fix it up.)

Non-isotopic frames don't trigger execution via word reference, are easier to pass around and manipulate.

>> f: make frame! :append

>> f.value: 10

>> append10: isotopic f

>> append10 [a b c]
== [a b c 10]

A non-isotopic frame--by virtue of not being isotopic--can appear literally in a block and the evaluator will run it when encountered, without the need for a word reference to trigger it.

>> do compose [(f) [a b c]]
== [a b c 10]

Due to this design, enumerating blocks will not accidentally run functions, the way historical Rebol and Red will:

rebol2>> foreach item compose [(:append) (:insert) (:change)] [
             if action? item [print "it's an action"]
         ]
** Script Error: item is missing its value argument

...since only isotopic actions run via word reference, and there are no isotopes allowed in blocks.

NIHIL is the name for the special case of isotopic BLOCK!, which is a parameter "PACK". It's simply an isotopic block with no values in it.

I've mentioned previously that packs are the basis of multi-return values, but if a situation is not sensitive to multi-returns then they will decay to their first element.

>> x: ~[1 2]~
== 1

>> [x y]: ~[1 2]~
== 1

>> x
== 1

>> y
== 2

>> append [a b c] ~[1 2]~
== [a b c 1]

As an example of the usage of multi-return values, consider FIND.

>> find "abcdefghi" "def"
; first in pack of length 2
== "defghi"

>> pos: find "abcdefghi" "def"
== "defghi"

>> [pos end]: find "abcdefghi" "def"
== "defghi"

>> end
== "ghi"

NIHIL being zero values cannot be used in an assignment, but if it's between expressions it will vanish.

>> 1 + 2 ~[]~
== 3

This is used as the return value for things like COMMENT and ELIDE.

Isotopic commas (like isotopic frames) cannot appear in blocks, so certainly different in that respect.

A non-isotope comma evaluates to an isotopic comma, which the evaluator discards... leaving the previous result:

>> meta ,
== ~,~

>> length of [10 + 20,]
== 4

>> 10 + 20,
== 30

It's similar to NIHIL in this respect. At one point COMMA! just evaluated to NIHIL but some problems were encountered with that, which I explain in the post you linked.

Isotopic ERROR! is a "Raised" Error

If you look at exception handling in Rebol and Red, it is extremely brittle. Their constructs will basically intercept any failure that occurs at any level in the enclosed code... including typos:

As a very simple example:

red>> attempt [data: read http://example.com]
== {<!doctype html>^/<html>^/<head>^/    <title>Example Domain</title>^/^/ ...

red>> attempt [data: read http://xaasdfaefafa.com]
== none

red>> attempt [data: readd http://example.com]
== none

So there's no real way to narrow intercepting errors that are a result of a specific call. Ren-C uses error isotopes to do this... where you can't store error isotopes or pass them most places, but special constructs like TRY and EXCEPT can handle them.

e.g. TAKE returns an error isotope when you try to take from an empty block. By default this will be "promoted to failure":

>> take []
** Script Error: Can't TAKE, no value available (consider TRY TAKE)

But before that promotion to failure (which can only be intercepted by SYS.UTIL.RESCUE), you can meta a raised error at the moment it is returned from a specific call:

>> meta take []
== ~make error! [
    type: 'Script
    id: 'nothing-to-take
    message: "Can't TAKE, no value available (consider TRY TAKE)"
    near: [meta take [] **]
    where: [take console]
    file: ~null~
    line: 1
]~

As it suggests, TRY will convert raised errors to null:

>> try take []
== ~null~  ; isotope

Original writeup here:

FAIL vs. RETURN RAISE: The New Age of Definitional Failures!

OBJECT! is the very experimental "LAZY"

This has turned out to be more of a rabbit hole than I first thought. I'm not sure if this is going to make it, but some experiments have been done with it:

Applications of Isotopic Objects

That's all the isotopes so far...

Indeed, which is why I asked this question, to know which are the meaningful ones so far!

OK, will do! But the issue is not so much outdated naming, as the variety of posts which explain why such-and-such a name is not used… except there are many such unused names by now, so it’s more confusing than anything else.

That makes sense, thanks! As it happens, I was wondering if it would be possible to include a FRAME! literally in a block, and this mechanism strikes me as an excellent way to incorporate that into the language.

(Also, I didn’t know ISOTOPIC existed… I was under the impression that there’s no general way to convert a value to its isotopic form.)

Ooh, that’s a nasty edge case… I can see why you’d want to prevent this!


At this point, I’m getting the general impression that many isotopes are things which get intercepted as soon as they’re created — unlike regular values, which only ‘change state’ when the evaluator processes them.

For instance, if f is a FRAME!, then when f is evaluated, it simply results in the FRAME! value, and that’s it. But if Ren-C evaluates f and gets back an isotopic FRAME!, then simply creating that value triggers further evaluation of its arguments and body. (Unless you’ve used a GET-WORD! like :f, which I presume has special handling to prevent that further evaluation.)

Similarly, if f is evaluated and results in an isotopic BLOCK!, then that immediately gets intercepted and decays to its first value (unless evaluation was triggered as the argument of a GET-BLOCK!). Or if it’s evaluated and results in an isotopic COMMA!, that immediately disappears on creation, whereas a regular COMMA! wouldn’t.

Is this a sensible way of thinking about it, or does this reasoning break down at some point?

When isotopes were first conceived, they could not be stored in variables at all.

Hence if you passed an isotope to a function, that function would have to use a ^META parameter convention to receive it, and hold it in an argument as a quasiform.

This had to give way to the concept that some isotopes were "stable" and could be stored in variables (since null, true, and false became isotopic--so did unset variables, and isotopic actions to signify the desire to execute through word reference.) Only the "unstable" isotopes (currently: error, block, comma, object) would go through some decaying process and refuse to be stored as-is in a variable.

Pragmatically, splices became stable so that you don't have to use ^META parameter conventions to pass them to things like APPEND:

>> f: make frame! :append

>> f.value: spread [1 2 3]  ; don't need to say `meta spread [1 2 3]`

>> run f [a b c]  ; variadic RUN, vs arity-1 DO that expects completed frame
== [a b c 1 2 3]

Given that block isotopes are unstable, you'll only receive them as the result of a function call (or evaluation of a quasi-block). A GET-WORD! fetching a variable will never yield one in the evaluator.

Sure; what I meant was that an isotopic BLOCK! doesn’t decay immediately if it’s to the right of a GET-BLOCK!, but rather ‘holds together’ long enough for its elements to be assigned to each of the constituent variables.

Ah, you mean SET-BLOCK!, such as [a b]:

SET-BLOCK! is not unique, in the sense you can write your own operations that use the ^META parameter convention.

dump-multi: func [^multi [pack?]] [
    for-each item unquasi multi [
        probe unmeta item
    ]
    return nihil
]

>> dump-multi find "abcdefghi" "def"
"defghi"
"ghi"

But if you don't use ^META and pass a typecheck for packs it will decay to the first value.

Gah, yes, I did mean SET-BLOCK!, sorry. (How that typo made it through two posts, I have no idea…)

OK, this is interesting. I didn’t realise you could do this. And I’m at a level where I can finally understand code like that, which is nice!

The one thing I don’t quite understand is the need for unmeta there. Insofar as I can see, find should return ordinary unquoted values, so why do they need to be unquoted further?

Multi-returns can themselves be isotopes, so the values in the block are META'd by convention.

>> pack [null 1 + 2]
; first in pack of length 2
== ~null~  ; isotope

>> meta pack [null 1 + 2]
== ~[~null~ '3]~

The PACK function also accepts @[...] blocks and takes them literally.

>> meta pack @[null 1 + 2]
== ~['null '1 '+ '2]~

So you can mix this up with control constructs, having different branches pack things up in different ways.

[op1 op2]: case [
    ... [
        print "This branch uses values as-is"
        pack @[+ -]
    ]
    ... [
       print "This branch needs evaluation"
       pack [operators.1, pick [- /] op-num]
   ]
]

SET-BLOCK! itself has a few tricks up its sleeve. "Circling" lets you pick which result is the overall result:

>> [pos @end]: find "abcdefghi" "def"
== "ghi"

>> pos
== "defghi"

>> end
== "ghi"

You don't need to name variables if you don't want to:

>> [_ @]: find "abcdefghi" "def"
== "ghi"

Optional values via refinement if you don't want to require them:

>> [a b]: pack [1]
** Error: Not enough values for required multi-return

>> [a /b]: pack [1]
== 1

>> a
== 1

>> b
== ~null~  ; isotope

>> [a /b]: 1
== 1

>> b
== ~null~  ; isotope

Ah, I see.

Wait, what’s this new thing now? Ren-C seems to have a huge amount of syntax…

More parts, more dialect options.

The @ types (THE-XXX!) cover something that isn't covered otherwise... non-evaluation of WORD!, PATH!, TUPLE!, GROUP!... and then there's BLOCK! as well.

>> @word  ; a THE-WORD!
== @word

>> @(gr o up)  ; a THE-GROUP!
== @(gr o up)

And what they're used for varies. For instance, in PARSE, it means "match fetched value literally".

>> rule: [repeat 2 "a"]

>> parse ["a" "a" "a" "a"] [some [rule (print "match!")]]
match!
match!
== "a"

>> parse [[repeat 2 "a"] [repeat 2 "a"]] [some [@rule (print "match!")]]
match!
match!
== [repeat 2 "a"]

Ah, interesting. So, referring back to our earlier discussion about non-evaluation of BLOCK!… could we say that GROUP! is to BLOCK! like WORD! is to THE-WORD!?

I suppose. But the more significant way in which they are similar (beyond one evaluating and one not) is just having the richness of choice of more parts that are already scanned and lined up for you.

It would always be possible to use something else to indicate which multi-return is the main return, like a TAG!

>> [a <return> b]: pack [1 2]
== 2

Even if you were stuck with only words, it could be positional... you could say odd slots aren't variables, but keywords, like either SKIP or RETURN:

>> [skip a return b]: pack [1 2]
== 2

But having more parts lets you be out-of-band while still carrying a binding, it's succinct.

>> [a @b]: pack [1 2]
== 2
1 Like

As an aid for myself, I made a summary table:

Type Isotopic form Usage of isotope Stable?
GROUP! splice Multiple values without a surrounding block
FRAME! action Trigger function execution
WORD! isoword Various special constant values
VOID trash Unset variables
BLOCK! pack Multi-returns from a function
COMMA! barrier Discarded by evaluator
ERROR! raised Errors raised from a function call
OBJECT! lazy (to be confirmed)

Did I miss any?

1 Like

I tweaked it (trash is stable, barriers aren't, "isoword" is being used but may become "antiword" in light of naming discussions)

I don't tend to write VOID as a type with an exclamation point, because right now it dovetails with the "void-in-null-out" convention, e.g. TYPE OF VOID is null. So voids effectively don't have a type.

Ah, thanks!

(I suppose trash must be stable by definition, since it’s the thing which is ‘stored’ in unset variables.)

Hmm, this is good to know.