Should Everything Have an Isotopic Form?

This thread is locked and preserved for its historical significance, and to show just how much progress was made in about 4 months in the establishment of generalized isotopes.

For context while reading: isotopes started as a cell flag on the nulls that carried them, to denote they were "heavy nulls".

So isotopes were no longer implemented with a flag, but a unique quote state (making it impossible to have an isotopic quoted WORD!, for instance...only an isotopic plain WORD!). It became clear that isotopes could be applied to more types, and in this post I argued for generalizing it... pointing out that we could make the "automatically run from words" behavior come only from the isotopic form of ACTION!.

We now take you back to July 16, 2022...

:hourglass:

A little too soon to declare victory, but... we may have a solution in our hands for the historical problem of splicing arrays, by allowing GROUP! isotopes to convey the splicing intent.

Isotopes are essentially "quote level -1" ... if you take something at quote level 0 and UNMETA it, you're allowed precisely one level of below-rock-bottom.

The isotopes are unfriendly: normal function parameters won't accept them, you cannot put them in arrays, and some of them have strange behaviors when assigned to variables.

So what would an isotopic group look like?

A thought I had inspired by BAD-WORD!s was that they might use tildes on the outsides:

>> unmeta [a b c]
== ~(a b c)~

But that's used for normal BAD-WORD! today, that you unmeta to make isotopes.

Which made me wonder, what if what we call BAD-WORD! today was actually a BAD-WORD! isotope, and you got them from unmeta'ing WORD! ?

>> unmeta 'something
== ~something~
    ; ^-- ornery, how what we call `~something~ isotope` is today

And then, what if you could make anything isotopic:

>> unmeta 1020
== ~1020~

This would break a number of things--and we've come to depend on reified BAD-WORD!s for some reasonably important purposes. But it crossed my mind and I tend to jot notes here for every single whim that comes to my mind, so there it is. :slight_smile:

Storing Isotopic Groups In Variables?

If you go back and review "Generic Quoting Makes Headway on MAKE OBJECT!" you'll see that we've been getting by not needing quotes on items that are inert.

You have to have a quote on (1 + 2) if the intent is to store a GROUP! in a variable, because it will evaluate otherwise:

make object! [group: '(1 + 2)]

But things like BLOCK! haven't needed them (and shouldn't):

make object! [block: [1 + 2]]

However, I think that isotopes are likely going to need to be stored in variables. You'll have to use ^META operations to get the value out of the variable.

Why couldn't it be an error to try and store an isotopic block in a variable? Well, there is some attempts to be friendly by higher-level operations like SPECIALIZE. So when you say

append-a-b-c: specialize :append [value: spread [a b c]]  ; or whatever...

What happened was that everything in the frame was marked as holding a special identity for unspecialized-ness. When the code block finishes running, it looks and notices value is a ^META parameter, and does the isotopic quoting for you.

If you don't have that feature, you have to know the convention of the parameter in order to assign it. That's not the end of the world, but it feels like a hassle. Being able to store an isotopic block in a variable enables that feature--as well as others.

So I Think Isotopes Are In For Some Kind of Reckoning

There's a lot of pieces to the puzzle, and they're all tied in together. But the good news is that the corpus of code is so big that the impacts of any change are seen, and so I can try to make sure nothing of value gets lost.

1 Like

But it crossed my mind and I tend to jot notes here for every single whim that comes to my mind, so there it is.

Corollary to this thought:

What if it's only isotopic ACTION!s that execute implicitly? And what if you can make an isotopic FRAME! that does the same?

This would mean that if you passed an action as a normal parameter to a function, it would have to be explicitly executed. And you wouldn't use GET-WORD! to defuse functions, you would use ^META.

ACTION! in BLOCK! couldn't be isotopic, so would not run

It would reduce the danger in this case:

for-each item block [
    if integer? item [...]  ; don't have to worry that ITEM consumes the block
]

You'd still have potential problems with enumerating objects and such, but the way it would guard you would be that the enumeration would force you to use ^META if you actually hit any isotopes.

obj: make object! [a: 10 b: func [x [text!]] [print ["Foiled!" x]]]

for-each [key value] object [
    print [value "is the value of" key]
]

That would catch you before the function could consume "is the value of". The FOR-EACH could complain and say it hit an isotope, so you'd need to use ^META.

This gives us the long-awaited get-what-you-pay-for safety in enumeration!

One Step Closer to GET-WORD! as Unmeta

A proposal in the air is that GET-WORD! act as UNMETA.

>> var: the '10

>> ^var
== ''10

>> :var
== 10

We could say that you could interchangeably use UTF-8 arrows for this:

>> ↑var
== ''10

>> ↓var
== 10

We might call them UP-WORD! and DOWN-WORD!, as there's not really what we'd say is a "GET" happening in the :var case.

A standalone colon could act as a free UNMETA, biasing it to having the meaning of the DOWN of :x (as opposed to the SET of x:)

>> append [a b c] : reduce [1 + 2 3 + 4]
== [a b c 3 7]

And in light of the current thinking on blocks, we'd get a shorthand for appending splices via isotopic blocks:

>> append [a b c] :[d e] 
== [a b c d e]

>> append [a b c] ↓[d e]
== [a b c d e]

This would mean we'd lose the GET-BLOCK!-as-REDUCE. Which is unfortunate. But REDUCE probably happens less often these days than splicing and UNMETA-ing.

Would this mean that things like REDUCE and COPY and such would need to take ^META parameters to handle isotopic blocks?

Urg. Not sure I think that's such a great idea, it undermines the point of the types being rare and not propagating that widely.

So you wouldn't write this:

append [a b c] reduce :[1 + 2 3 + 4]

You'd write:

>> append [a b c] : reduce [1 + 2 3 + 4]
== [a b c 3 7]

It would be technically possible to make things like REDUCE operate in the ^META domain, but I (think) it's probably better if only the functions that directly ascribe meaning to the isotopic state are actually meta.

1 Like

The gist of the above idea is to make the following code safe:

for-each item some-block [  ; isotopes won't be found in a BLOCK!
    switch type of item [  ; no need for `:item`
        action! [print "It's an ACTION! at quote level 0"]
        integer! [print "It's an INTEGER! at quote level 0"]
    ] else [
        print "It's not an action or integer (or is quoted)"
    ]
]

Historical Redbol makes you have to be very cautious and do tests like action? :item, where the colon is required to suppress the implicit evaluation. It also has to worry about UNSET! (and in Rebol2, "armed" errors as well.) This is a pleasing solution to the problem.

(Of course, if you are enumerating an OBJECT! or MODULE! you will encounter the isotopic form of the action in the values. So you'd have to say for-each [key ^value] [...] for full coverage. But that again has pleasing safety properties, because once you ^META something it won't be isotopic anymore...so had it been an isotopic ACTION! that would run implicitly on word fetch, it will now be a quote level 0 action that won't!)

But There's a Glitch If ACTION! at Quote Level 0 Is Inert

The system relies on plain ACTION!s translating to invocation in several places... in particular the API.

Let's imagine we live in the world where print_isotope is allowed to be a function isotope:

 REBVAL* print_isotope = rebValue("get/any 'print");
 rebElide(print_isotope, "[{Print} {me}]");

That causes a runtime error. Because rebElide() internally makes something equivalent to a BLOCK!. And isotopes can't be put in BLOCK!s. :-/

The same problem would affect people trying to make code with a COMPOSE:

 >> do compose [(get/any 'print) [{Print} {me}]]
 ** Error: COMPOSE can't put ACTION! isotope in BLOCK!

So you have to ^META that isotope and get an ACTION!. But if you do that, then the pitch above was that the action would not run:

 >> do compose [(^print) [{Print} {me}]]
 == ["Print" "me"]

You'd need to throw in some additional operation, like REEVAL:

 >> do compose [reeval (^print) [{Print} {me}]]
 Print me

That's not great. We're saying you are forced to pay for a WORD!-lookup to lean on the WORD!-dispatches-action-isotopes behavior, because we don't have any other way of running a plain ACTION! when nothing is allowed to go lower than quote level 0.

But Can't ACTION! vs. WORD!-Fetched-ACTION! Be Different?

Our safe FOR-EACH has an ACTION! referenced by item, a WORD!

The block wants to run a raw ACTION!.

Couldn't we just say that actions fetched by words must be isotopes to run, and then say that a non-isotopic ACTION! that's not referenced through a WORD! will execute?

Sounds good, but then what about this:

>> obj: make object! [foo: func [] [print "FUNC makes Isotopic ACTION!"]]
== make object! [
    foo: ???  ; solve for ???
]

Which is to ask: what value sits in that slot and doesn't run an ACTION!, but produces an action isotope that foo can receive?

We need a single atomic value at a quoting level no lower than 0 which produces ACTION! isotopes when evaluated.

That can't be ACTION! itself... if we've said that plain ACTION!s need to run when they are found literally during execution of a block.

What About Terminal Slash?

I came up with an idea some time ago that terminal slash could be used on paths to say that they would execute. For words that pointed at ACTION!s already, this was just commentary:

>> append/ [a b c] 'd
== [a b c d]

But it could be used to retrigger things, like a REEVAL would--but not keyword-dependent:

 >> (specialize :append [value: 'd])/ [a b c]
 == [a b c d]

Due to some clever mechanisms of path and tuple compression, this form of PATH! is accomplished in a single cell.

Maybe this explicit execution is the answer for those who wish to compose actions and run them?

 >> do compose [(^print)/ [{Print} {me}]]
 Print me

The API could offer some operator like rebA() for (rebACTIVATE()) that would slashify your actions:

 REBVAL* print_action = rebValue("^print");
 rebElide(rebA(print_action), "[{Print} {me}]");

This would mean a plain ACTION! could evaluate to its isotope.

Well, it's an idea. Just kind of trying to inventory the possibilities here...

My gut feeling on this is that plain ACTION! should not need this rebA() to run. We have the nice generic quoting aspect to escape it. But unfortunately we don't have a nice generic unquoting mechanism to annotate things as needing to be isotopes.

I think I'd rather make a separate GET-ACTION! datatype that becomes isotopic, than screw things up too badly in common use.

2 Likes

So this hypothetical "GET-ACTION!" type would have to be the ^META of an isotopic action. Because let's say you're trying to POKE an action isotope through the API, or via the analogous situation with a COMPOSE:

 ; we assume here that FUNC returns isotopes, so x: func [] [] executes on X
 ;
 ; Remember, BLOCK!s can't hold isotopes, so we have to ^META it
 ; (unless an exception was made for ACTION! isotopes to be auto-^META'd)
 ;
 do compose [poke obj name (^ func [] [print "I'm a method in an object"]])]

If the ^META of an ACTION! isotope is ACTION!, and actions run when encountered in blocks by default, then what you'd wind up with there is actually running the method...instead of assigning the isotope. Which is why it would have to be GET-ACTION!, so this could properly evaluate to the desired isotope to poke.

But That Is Ugly

...it means instead of one ACTION! type you have three states to worry about.

  • Plain ACTION! that does not run when fetched via WORD! or TUPLE! (but runs when encountered as-is in DO of a BLOCK!)

  • Isotopic GET-ACTION! that runs when fetched via WORD! or TUPLE!

  • Plain GET-ACTION! that becomes an isotopic GET-ACTION! when encountered as-is in DO of a BLOCK!

But in such a world...when would you ever encounter a plain ACTION!? Everywhere you looked there would be GET-ACTION!s.

 >> action: ^append

 >> type of action
 == #[datatype! get-action!]

This sucks, and makes it pretty clear that GET-ACTION! should just be ACTION!...which has exactly two forms, normal and isotope. (and we can then call the isotopic form of ACTION! an "activation")

Which is to say, that in the API (or a COMPOSE), if you haven't assigned an action isotope to some WORD! you can use, you need to mark the slot for execution one way or another:

>> do compose [(^print)/ [{Print} {me}]]
Print me

>> do compose [reeval (^print) [{Print} {me}]]
Print me

>> do compose [apply (^print) [ [{Print} {me}] ]]
Print me

A nice aspect of this is that the terminal slash is efficient, and will work whether you are splicing an ACTION! or a TUPLE! or a WORD! in that slot.

It's Necessary To Preserve Invariants

The following code neds to guarantee leaving VAR in the same state it was found:

temp: ^var
var: unmeta temp  ; Plan is that :TEMP will be synonym for UNMETA TEMP

This should also leave VAR in the same state as it was found:

temp: ^var
do [var: (temp)]

You really lose your bearings if those things don't hold true.

What About the Parallel Complexity for Splices?

Laying down those rules definitely does suggest that a "splice" cannot be a [...] isotope, but rather a :[...] isotope (GET-BLOCK! might be a bad name, it would be an UNMETA-BLOCK! or UN-BLOCK! or DOWN-BLOCK! or similar).

I don't think splices being a different type is the same kind of problem as having a secondary superfluous action type. The scenarios are pretty different.

But quickly to the topic question... which is now understood a bit better.

"Should Everything Have an Isotopic Form?"

No!

I believe the rule I came up with is the correct way to put it: the only types that have isotopic forms are those whose evaluation is to their isotope. That gives us the invariants I outline above.

That gives us:

  • ~bad-words~

  • action!

  • :[...] (whatever we call these blocks)

  • error! (which we now know, literal errors will evaluate to their isotopes)

That's all for now, anyway.

1 Like

So I think this idea has turned out to be too limiting.

I've backed out the code that stopped ACTION! from executing in a block, so it's back at the point where if you compose an action literal value into a block, it just runs. (Some interesting related developments were kept, so it was not all for naught.)

I've observed that under the new concept of :xxx behavior, a :(...) styled group could be used to make anything an isotopic form, even if it doesn't have a "GET-XXX" form.

>> make object! [demo: unmeta block!]
== make object! [
     demo: :(#[datatype! block!])
]

So even if ACTION! would typically run, you could put a quoted action inside of a :(...) to get an isotopic form of the action.

Or...could There Be A Notation for "anti-quoting"?

An old half-baked thought was toying with the notation of surrounding things with tildes to denote isotopes. But I remarked that really, we should not be thinking about isotopes as having any notation. Their existence is only measured by parameter conventions that "quote them up into the visible spectrum".

But...there could be something similar to QUOTED!...a "container" that was a generic ANTI-QUOTED! (or what I've been calling "BAD!"), which when evaluated gave an isotope.

>> 'word
== word

>> ~word~
== word  ; isotope

>> ~[a b c]~
== [a b c]  ; isotope

In this world, we could say that lone ~ is the representation of a BAD! blank, and blank isotopes could be the contents of unset variables, what I've been calling NONE. :thinking:

This is notably not just another level of quoting, because these would be entities which could themselves be quoted.

>> quote first [~baddie~]
== '~baddie~

So this datatype would be something that would replace BAD-WORD!, and would give better visual indications of isotopes:

 >> make object! [x: ~xxx~, y: '~yyy~, z: spread [a b], f: does [print "HI"]]
 == make object! [
     x: ~xxx~
     y: '~yyy~
     z: ~(a b)~
     f: ~#[action! []]~
 ]

There you can see that X actually denotes that it holds a WORD! isotope, while Y has a way of conveying that it actually holds the BAD!-word-container ~yyy~. Z holds an isotopic group (a "splice"), and F holds an action that will run through word reference (an "activation").

It seems more harmonious than if we stick with the current BAD-WORD! only design, and go the :(...) route, should : be deemed useful for UNMETA-GET.

 >> make object! [x: ~xxx~, y: '~yyy~, z: spread [a b], f: does [print "HI"]]
 == make object! [
     x: ~xxx~
     y: '~yyy~
     z: :(a b)
     f: :(#[action! []])
 ]

Harder to make sense of, and less performant (the BAD! state would be encoded as part of the quote byte, e.g. we have 127 levels of quoting instead of 255, to chew out an extra bit for "are you bad, or what?").

Would This New Menagerie Pay Off?

This started with trying to find ways to generalize the basic mechanics of unsets, as well as how to have a taken branch return NULL...yet not trigger an ELSE. And it's taken off from there.

Has the expansion in scope been good? Well...

These are the early moments of isotopic actions, and they introduce some complexities, but they're being figured out.

I put forth the question of isotopic typesets, where if you could pass something like FIND a DATATYPE! isotope, it might interpret that as looking for instances of the type instead of looking for the datatype itself.

Rebol has always used datatype sensitivity as a cue for behavior, and isotopes offer generalized routines a new dimension of "I don't mean it literally..." and that non-literalness is given a boost by preventing the isotopic values from appearing in blocks. I don't think this should seep too much into user consciousness...but it helps the core functions a lot.

Really it looks like things have reached the point where generalized isotopes are the best answer.


UPDATE: Trying some experimentation shows some promise, but definitely issues to consider with making it possible to have isotopes of everything.

  • We already know that ERROR! isotopes can't be stored in variables, because the act of trying to store one without ^META-ing them will raise a "non-definitional" failure.

    • This may extend to prohibiting storing other kinds of isotopes in variables--even perhaps most kinds (?) I don't know if there are particularly great arguments for or against letting people store things like block isotopes (splices) in a variable vs. raising an error.
  • Previously it was the case that whenever you took the ^META of something, only NULL could be falsey...because the ^META of NULL is NULL. That is what makes the FOR-BOTH example work:

      for-both: lambda ['var blk1 blk2 body] [
          unmeta all [
              meta for-each (var) blk1 body
              meta for-each (var) blk2 body
          ]
      ]
    

    But if a body evaluation of the FOR-EACH can return something like a BLANK! isotope or a LOGIC! false isotope, then META'ing those will be false and act like BREAKs. This would be particularly chronic if ~ was taken to be the way of generating an isotopic BLANK!, and isotopic blank was thus used for unset (leading to the ~ for unset variables in object renderings seen today).

    You could fix that with double meta and double unmeta, which would take something like a blank isotope to a blank (falsey) and then a quoted blank (truthy) so truly only the NULL would remain as NULL and be falsey, but... :nauseated_face:

      for-both: lambda ['var blk1 blk2 body] [
          unmeta unmeta all [
              meta meta for-each (var) blk1 body
              meta meta for-each (var) blk2 body
          ]
      ]
    

    Well... this isn't actually that bad a solution to the problem. Saying it's unthinkable is kind of like a mathematician rejecting an equation because it has something squared in it. There could be meta2 and unmeta2 operators, or meta+ and unmeta+...and if you know why you're taking the extra step that's fine. (Unfortunately the idea of operators ^^ and :: capturing this succinctly doesn't currently fit, because ^ does not pass through void... however, perhaps ^ and ^() could be distinct in this manner)

      for-both: lambda ['var blk1 blk2 body] [
          :: all [
              ^^ for-each (var) blk1 body
              ^^ for-each (var) blk2 body
          ]
      ]
    

    Or maybe instead of META which doesn't contain the deisotoped non-nulls enough to keep them from "reacting", you could put things in blocks. So NULL => NULL and VOID => VOID, but _ => [_], and by virtue of being in the box it's not falsey. So box and unbox?

    Or... maybe we just call the 2-meta form metaquote and unmetaquote, to emphasize that you are pushing things up through the level so it's quoted in order to truly stop the datatype from "reacting". That could be a more semantic and less mechanical way of saying the same thing--without needing to allocate a series.

      for-both: lambda ['var blk1 blk2 body] [
          unmetaquote all [
              metaquote for-each (var) blk1 body
              metaquote for-each (var) blk2 body
          ]
      ]
    

    Another option would be using a version of ALL that only considered NULLs to be falsey.

      for-both: lambda ['var blk1 blk2 body] [
          unmeta all/predicate [
              meta for-each (var) blk1 body
              meta for-each (var) blk2 body
          ] ^x -> [not null? x]
      ]
    

    Anyway...it's just sort of the price you pay for completeness, and having more than one signal for falseyness (which is its own question... I can't say for sure that (1 = 2) shouldn't return NULL and BLANK!s shouldn't be truthy...sigh)

    It's just what happens when we can no longer guarantee that the only isotope that generates a falsey meta product is NULL.

  • The handling of BAD-WORD! previously was geared toward system specific purposes, like ~null~ generating a ~null~ isotope with special behaviors. But under the generic scheme, ~null~ produces the null WORD! isotope...and people might have ideas for how to use that in some non-literal-sense of WORD! in a way different from the system ideas for ~null~ and ~void~ behaviors--where these applications could compete.

    • It's not that different from saying "no, you don't have full control of ERROR! isotopes"... but WORD! is more foundational and it may have more obvious out-of-band signaling applications.

    • The system could take something less "in demand" like ~@void~ vs. ~void~, but it feels a bit lame to be demoting the original purpose of "BAD-WORD!s"...they were supposed to be clean-looking precisely because they are of frequent concern when doing things ^META.

    • One strange idea would be to say that the system purposes actually make the BAD! states the ^META states, e.g. ^(comment "hi") would give back ~void~. This would be an outlier, because there's no such thing as ~~void~~ to produce an isotopic bad-thing; it's merely a way of describing a state that has no other description. So if you wanted to "unevaluate" it, you'd have to do that with (unmeta '~void~) or a reference to the void function itself to produce the outlier state.

So definitely no shortage of thinking points, here... :exploding_head:

This worldview really requires you to get your head around the difference between "unevaluation" and "meta"...

  • If you want to unevaluate a word! isotope of the word foo, then you need something that when evaluated will give you the isotope of the word foo. So the unevaluation in this world would be ~foo~.

  • If you want to meta a word! isotope of the word foo, then you get the plain (non-isotopic) word foo at quoting level 0. If you choose to put that through normal evaluation, it will run the word as you would expect...looking up the value or function assigned to foo.

Before, I was trying to make unevaluate=meta ... which gave that rule that "only things which evaluate to their own isotopes can be have isotopic forms". That rule turned out to screw up things like ACTION!...which turn out to be more useful if they are invoked when evaluated. And of course it can't work for things like WORD!, which need to do lookups in evaluation.

With unevaluation and meta becoming distinct, I don't know if we want to just say that QUOTE goes ahead and acts as unevaluation...e.g. quoting an isotope makes it BAD!, and the next steps above that make it quoted bad:

>> ~foo~
== foo  ; isotope

>> meta ~foo~
== foo

>> meta meta ~foo~
== 'foo

>> quote ~foo~
== ~foo~

>> quote quote ~foo~
== '~foo~

The problem with this is that if you say QUOTE of something, and the result is not QUOTED!, that might feel wrong. I don't know if that justifies introducing this separate term of UNEVALUATE/UNEVAL...but...arrgh, it probably does.

So we could unify "unevaluation" and "meta"... if when you receive an isotopic value as a ^META argument it will be BAD!.

This actually resembles the status quo where there were "bad-word! isotopes", e.g.:

>> ~foo~
== ~foo~  ; isotope (vs. plain WORD! of `foo` isotope)

In this model, there would be no WORD! isotopes, or ACTION! isotopes...just BAD! WORD! isotopes, and BAD! ACTION! isotopes (etc.) Example for INTEGER!:

>> ~1~
== ~1~  ; isotope (vs. plain INTEGER! of `1` isotope)

>> meta ~1~
== ~1~

A ^META Parameter Could Thus Only be One of 3 Things

  • A NULL if the input was NULL

  • A QUOTED! if the input was normal (this would cover if the input was "normal bad", e.g. just some random ~foo~ value picked out of a block, this would be received as '~foo~).

  • A BAD! if the input was isotopic (e.g. the input was both isotopic and bad, as this is proposing that's the only kind of isotope)

This Would Mean META Would Be the Same As UNEVAL

Dropping UNEVAL as a separate concern sounds good on the surface. This lets us have generic isotopes -and- gets us back to the following being a no-op, regardless of VAR's initial state:

temp: ^var
do [var: (temp)]

Plus, if BAD! values acted like quoteds and were always considered truthy (even a BAD! LOGIC! of false or a BAD! BLANK!) this would put NULL back as being the only falsey state for a ^META value. So things like FOR-BOTH would still work as expected with the following implementation, where NULL would be the only loop result to lead to breaking:

    for-both: lambda ['var blk1 blk2 body] [
        unmeta all [
            meta for-each (var) blk1 body
            meta for-each (var) blk2 body
        ]
    ]

That's one of the pinnacles of the Ren-C design...and I certainly place importance on not making that any uglier.

Sounds Pretty Good... Anything Wrong With It?

(...Insert usual reminder of "essential complexity can't be erased, only redistributed" here...)

Once you've ^META'd something, you get something you always have to unwrap (if it's not null). And you can't directly UNMETA things without wrapping them up in BAD! first.

Getting more unwrappings and wrappings involved may be the right number to have--but it gets a little more awkward.

Think of the perspective of the author of something like SPREAD...

SPREAD wants to take in a BLOCK! and make a GROUP! isotope out of it. This used to be as simple as unmeta as group! block on its argument. But now GROUP! represents the bottom of the META pile (as it is the bottom-most QUOTE level). You'd have to do something morally equivalent to unmeta make bad! as group! block, because only BAD! can be unmeta'd below quoting level 0.

(unless you're willing to make UNMETA return something that wouldn't META back to what you gave it initially, e.g. UNMETA of plain GROUP! gives you an isotopic bad group, where META'ing that gives a bad-block. That doesn't sound good.)

This side-step SPREAD takes onto "the bad path" won't ever come back to GROUP! through meta and unmeta operations alone. Because that bad group isotope will be meta'd to a plain bad group, and then up to a quoted bad group and so forth.

Then think of the perspective of the author of something like APPEND...

APPEND has decided to take a ^META argument for the appended value instead of a normal one. The reason is because it wants to differentiate spliced GROUP! from plain GROUP!.

Now you get either a QUOTED! block or a BAD! group... it's in a container in both cases, and you have to get it out. Either UNQUOTE or UNMETA will get you back your plain block from the QUOTED!, but neither will extract the group from the BAD!, so what will?

(So it's pretty clear that we can't be calling these things "bad"... fully legitimate code with branches testing for this wouldn't make any sense.)

It's A Somewhat Tough Call

It seemed a nice property of ^META that since there were no quoted isotopes, we really could look directly at the result of a ^META and know any non-quoted thing implied the original input was isotopic.

[^x]: something then [
    if quoted? x [x: unquote x, handle x]
    else [handle x]
] else [
   ; null handling
]

Arguably it's just one more step:

[^x]: something then [
    if quoted? x [x: unquote x, handle x]
    else [x: unbad x, handle x]
] else [
   ; null handling
]

I Feel Like I'm Just Going to Have To Try It

It's sort of too hard to pre-reason about this. There are enough advantages to certainly make it worth a shot.

So I'm going to try the version where all isotopes are also "bad" and see how awkward it is, and what sorts of patterns emerge. I'll keep it as fluid as possible, so the C code will say things like Is_Meta_Of_Splice() instead of Is_Bad_Group(), so it will be easier to change if it turns out to be wrong.

1 Like

Arrgh.

The goal here is to make pleasing parts that compose well, letting novices build clever language constructs out of solid-feeling bricks. By this point I shouldn't have to stress that this is really, really hard to make simple.

:face_with_head_bandage:

But...slogging through it some more...


As I've mentioned, this doesn't make everything easier...it introduces a level of unwrapping whether you are dealing with something that was isotopic or if it was plain.

But the fact is that fully generic quoting--including of isotopic things--isn't just useful, it's foundational. And things like the META-WORD! fuse together accessing a potentially isotopic value with a quoting operation. If QUOTE and META were not mostly the same thing conceptually, we'd be hard pressed to make a quoted isotope... even if we allowed QUOTE to take isotopes as parameters, because getting those isotopes would be a challenge.

To demonstrate: let's say META decided to go its own way and not give back something that would evaluate back to the isotope. But QUOTE decided it would take isotopes, and give you back something that evaluated to an isotope (effectively becoming "UNEVAL"):

>> ~something~
== something  ; isotope

>> meta ~something~
== something

>> quote ~something~
== ~something~

So META's result can be used without unboxing it, while QUOTE's result is ready to be poked into a COMPOSE or something of that sort. Different purposes, so maybe okay, right?

But there's a META-WORD! but no corresponding operator that means "make quoted" (a quoted word just means "be quoted"). So you've got a pretty big feature gap

 >> x: ~something~
 == something  ; isotope

 >> ^x
 == something

 >> quote x
 ** Error: x is a WORD! isotope, use ^x to access it

You'd have to get awkward:

 >> quote unmeta ^x
 == ~something~

 >> quote get/any 'x
 == ~something~

This shows a pretty big pain point for having the ^META meanings and QUOTE meanings divergent... not just the mechanical pain, but the conceptual pain of understanding what the heck is going on here.

So I'm getting fairly certain we'll have to accept the situation of saying that ^META of isotopes are wrapped up in the thing I am currently calling "BAD!" (but clearly it shouldn't be called that)

>> x: ~something~
== ~something~  ; isotope

>> ^x
== ~something~

...But What About The NULL Situation?

When I suggested making ^META of something like a word isotope be a ~word~ instead of plain word, I claimed it had an advantage...such as by making it easier to use in something like COMPOSE to reconstitute the original variable back:

temp: ^var
do compose [var: (temp)]

But there's the twist that META of NULL is NULL. So that COMPOSE will complain about it (which is good, it forces you to decide what you really meant, and if that was a special case missing handling).

You can't solve this by putting a quote mark on the group in the compose, because that will mess up non-NULL values:

temp: ^var
do compose [var: '(temp)]  ; if var starts as foo, it will wind up as 'foo

Quoting produces a standalone tick mark instead of a NULL for a NULL input, which provides the necessary properties of being both reified (so it can be in a block) and reproducing null in evaluation:

>> quote null
== '

>> '
== null

But a quoted null (single tick mark) does not trigger ELSE and isn't falsey.

For Completeness: What About A (NULL => ') Construct?

If QUOTE and META were similar in all other aspects, we could add a missing construct to fill in this gap. Something that turned NULL into a single quote and passed through all BAD! and QUOTED! as-is.

Blunt name for starters...nullquote. For the moment, we will imagine it's tailored to fit this situation, and will warn you if the thing it's spitting out as a result can't be UNMETA'd:

>> nullquote null
== '

>> nullquote first ['3]
== '3

>> nullquote <something>
** Error: NULLQUOTE only accepts BAD!, QUOTED!, and NULL

The concept would be you could combine this with ^var to turn it into a generalized "unevaluate" operation that could also work in COMPOSE scenarios:

temp: nullquote ^var
do compose [var: (temp)]

-or-

temp: ^var
do compose [var: (nullquote temp)]

You could also try being explicit:

do compose [var: (temp else [the '])]

do compose [var: (temp else [''])]

do compose [var: (temp else [quote null])]

The current idea behind REIFY is that it would produce ~null~ out of a NULL. It doesn't produce a quoted null because it's trying to put in a placeholder that represents the idea of a non-quoted null (which isn't possible, so that's why it makes a visibly jarring choice). But in the case of these assignments under evaluation, it would produce an isotopic null and do the correct thing.

What About The Reverse... (' => NULL) ... nulldequote?

If we made the ^META operations never return NULL, then that produces a different situation:

>> x: null

>> ^x
== '

>> nulldequote ^x
; null

It should be obvious that the other way around is preferable.

META'ing defaulting NULL => NULL has systemic benefits. I'll play the broken record about FOR-BOTH:

for-both: lambda ['var blk1 blk2 body] [
    unmeta all [
        meta for-each (var) blk1 body
        meta for-each (var) blk2 body
    ]
]

Just in general, being able to find out if an operation--even a ^META one--is NULL is a good test.

Conclusions

:question: :question: :question:

It's clear that diligence is needed to reduce the total number of concepts in play--to the extent possible--without sacrificing functionality.

  • I was a bit spooked by the possibility of having three distinct concepts: QUOTING, META, and UNEVAL... all doing similar but slightly different things.

    • Unifying QUOTE and UNEVAL has the somewhat uncomfortable consequence of saying that the result of QUOTE isn't always a QUOTED! thing.
  • So I suggested making META on isotopes give back something that when evaluated would produce the isotope back.

    • This meant the META of something like an isotopic WORD! could not be a WORD!, because that would not evaluate back to an isotope...it would have to be in a wrapper (the BAD! wrapper, indicated by ~<leading and trailing tildes>~

    • This allowed NULL to be the only falsey output from META, so long as this wrapper type would be truthy even if it was a wrapped BLANK! or wrapped LOGIC! false.

  • That mostly nixes the need for UNEVAL, but with a loophole surrounding NULL behavior when the unevaluated thing was to be used in something like a COMPOSE

    • As an edge case, it doesn't seem too horrific...especially since NULLs now tend to raise errors to draw attention to improper uses.

    • If you can turn a NULL into a quoted null (') one way or another, the loophole is filled in the cases where it matters.

Now Needed: More Techniques, and Better Names

So if ^META is going to be coming back BAD! or QUOTED! or NULL... then there's going to have to be some ways of dealing with this.

I feel like there needs to be some sort of mechanic to make this pattern easier:

 x: ^(some operation)
 switch type of x [
     null [...process null case...]
     bad! [x: unbad x, ...process isotope case...]
     quoted! [x: unquote x, ...process normal case...]
 ]

(And of course UNBAD is terrible. I wondered if maybe ~asdf~ could be called a "semiquoted" or "quasiquoted" WORD!, so that UNQUOTE could be used to remove the ~'s from it. But that probably just makes things more confusing instead of less (and introduces '''~asdf~ as a "quoted quasiquoted word")

We've got a few options:

 (some operation) then ^x -> [
     if quoted? x [
         x: unquote x, ...process normal case...
     ] else [
         x: unbad x, ...process isotope case...
     ]
 ] else [
     ...process null case...
 ]

Anyway, this looks like where things are at right now, and is what has to be improved upon. The rationale feels pretty sound. Guess it just needs to develop from here.