The Implications of ^META Producing QUASI! from Isotopes

TL;DR - GET-WORD! to Fetch ACTION!s Will Be Unchanged

  • In the beginnings of the action isotope proposal, I thought that it would be perfect to move away from GET-WORD! to ^META-WORD! to reconstitute actions.

    • The ^META-WORD! would give back something you could put into variables that was not word-active...hence easier to work with.

    • At that time, what ^META of an action isotope gave back was a plain ACTION!

  • With the rise of generic isotopes and more examples of their functionality, it became clear that ^META of an isotopic form would return a quasiform.

    • Quasiforms have a unique place in the system as a representation of "isotopic status in suspended animation"

    • Higher-order manipulations are not supposed to disregard the QUASI!-ness as if it's not there. (The form was initially even more pejorative, called "BAD", to draw attention.)

  • The need to have generic operations on ACTION! that compose or manipulate them suggests the best ergonomic is to make isotopic actions able to decay to plain ACTION! to facilitate these compositions.

    • QUASI!-forms will not (and should not) do this decay, leading to unpleasant patterns like apply unquasi ^add [1 2]

    • The preferable syntax of apply :add [1 2] can leverage the more malleable idea of isotopic decay to allow APPLY to accept an action isotope as its applicand.

  • This decision ripples out to other operations which may be on the fence, such as CHAIN, which is now decided to work as chain [:add, :negate] instead of as chain [^add, ^negate] as was prototyped in an early commit.

Here was the line of reasoning to get there, which says the same thing in many more words.

The Construction of Historical CHAIN

So far, if you wrote something that chained two functions together, like:

>> double-plus-twenty: chain [lambda [x] [2 * x], specialize :add [value2: 20]]

>> double-plus-twenty 500
== 1020

What actually happened was two steps. CHAIN is a higher-level function, that calls REDUCE of the block to produce ACTION! values, followed by calling a lower-level CHAIN* native that expects a block of just actions.

This was nice because it meant that the mechanics inside of CHAIN* didn't have to worry about having some implementation of REDUCE inside of it. It could focus on what it did...taking in a list of ACTION!s, and building an aggregate ACTION! that ran them as a pipeline.

...Enter ACTION! Isotopes...

The first experiment was that if you did something like ^append on an action isotope (like the word APPEND would look up to), you would get a plain ACTION! back.

The previous CHAIN* would thus still work, so long as you changed it to ^META whatever you wanted to chain together:

reversed-append: chain [^append, ^reverse]

But a design decision emerged that when you ^META an isotope, you get a QUASI! form. This is one of the reasons that even though isotopes have "no representation", they are shown with the QUASI!-marks in the default terminal (plus a comment).

>> spread [a b c]
== ~(a b c)~  ; isotope

>> ^ spread [a b c]
== ~(a b c)~

Generally speaking, this is a good thing. If you have to ^META something just to reify it (with the intention to UNMETA it down the line and get the isotope back), then this is informative. It's why you have to take special action to throw the quasi status away (UNQUASI).

But now if you feed the lower-level CHAIN with an array out of REDUCE of ^META forms, you'll have a block of QUASI!-actions:

>> reduce [^append, ^reverse]
== [~#[action! {append} [...]]~ ~#[action! {reverse} [...]]~]

GET-WORD!s couldn't be handled by a plain REDUCE at all, because they'd give back isotopes--illegal in blocks.

"So What's The Problem? Make CHAIN Take QUASI-ACTION!"

Mechanically, that would work.

But if it takes QUASI-ACTION!, should it still take plain ones too?

Let's stop and think about what the difference between an ACTION! and a QUASI-ACTION! represented in a block are.

A plain ACTION! in a block is something that when evaluated, will run the action.

>> compose [(unquasi ^add) 1000 20]
== [#[action! {add} [...]] 1000 20]

>> do compose [(unquasi ^add) 1000 20]
== 1020

A QUASI! action will evaluate to produce an isotopic action. The current proposal for isotopic actions is that they are unfriendly as most isotopes are, so that they are hard to assign to WORD!s. But techniques would sneak past that...namely having purposeful function generators wrap them up in isotopic objects designed to communicate with SET-WORD!s/etc. and approve the assignment:

>> ^add
== ~#[action! {add} [...]]~

>> do compose [(^add)]
== ~#[action! {add} [...]]~  ; isotope

>> do compose [(^add) 1000 20]
== 20  ; didn't run the action, it evaluated isotopically and vanished

So you'd use different pieces in different ways.

When you look at it this way, it kind of makes it seem like QUASI-ACTION! should be the currency of action that CHAIN would want. Consider the difference between these two at-a-glance:

[#[action! {append} [...]] #[action! {reverse} [...]]]

[~#[action! {append} [...]]~ ~#[action! {reverse} [...]]~]

The first array of plain ACTION!s comes off as something of an incomplete sentence. It's like you've written [append reverse] and you're building up a statement where the first argument to an APPEND is being REVERSE'd:

>> code: [#[action! {append} [...]] #[action! {reverse} [...]]]

>> append code [[a b c] [d e]]

>> do code
== [c b a [d e]]

The second array of QUASI!-actions is kind of clearly not a sentence. It's more like a collection of isotopic actions in "suspended animation". If you REDUCE-EACH it, you'll be able to digest it back into action isotopes.

Looked at in just this light, the QUASI! actions seem like a pretty good currency for CHAIN* to accept. Though it's behind the scenes a bit.

But Such Decisions Ripple...

So now let's look at something that doesn't take its input in an array, like APPLY. Today it takes an ACTION! as an argument:

apply: func [
    applicand [action!]
    args [block!]

The goal of isotopic actions is to have a way that generic parameters or things that enumerate blocks don't have to worry about turning a WORD! into something that runs actions unexpectedly. It's not really as relevant to things like APPLY, because they know things like their APPLICAND are an ACTION!. This isn't a burden...and in fact, it can be annoying if you plan on calling the function in the body to receive it inertly.

(Quick reminder: it's also a solution to a problem that Boris repeatedly mentions, e.g. REPLACE can't distinguish between an action you pass it to look for literally vs. one you want to run as part of the replacement. So it's more than just a safety tool.)

Anyway...things are all a bit turned on their head, because it's kind of not clear how to get a plain ACTION!. Things like FUNC create action isotopes (wrapped up in an object to denote they were just generated, and so it's likely they were intended to be assigned somewhere soon). And then ^META operations make quasi forms. So you've got choices like:

>> :append
== ~#[action! {append} [...]]~  ; isotope

>> ^append
== ~#[action! {append} [...]]~

 >> func [] [return 1020]
 == ~#[action! [...]]~  ; isotope

The only ways to make a regular-old ACTION! is "unquasi meta" or a "reify get"

>> reify :append
== #[action! {append} [...]]

>> unquasi ^append
== #[action! {append} [...]]

This got me to wondering if one of the decaying behaviors of isotopes would be that action isotopes decay to normal actions when passed to function arguments that don't take isotopes (but that would be happy to take actions). That's within the M.O. of isotopes, just generally.

(Decaying a QUASI!-ACTION! makes less sense. All ANY-VALUE! types are supposed to be robust bricks and trustworthy, that includes QUASI! and QUOTED!--e.g. none of the infamous "lit-word decay").

So that suggests people would call APPLY with a traditional GET-WORD!, pass an isotope, let it decay:

>> apply :add [300 4]
== 304

But then this gives us a bit of an incongruity. You're ^META-ing functions to put them in a CHAIN, and :GET-ting them to use them with an APPLY. :frowning:

This feels annoying. So what it makes me think is that the higher-level CHAIN itself processes the array with something besides a REDUCE. e.g. it does some kind of REDUCE-EACH, and accepts either ACTION! values or ACTION! isotopes, the way something like APPLY would. It canonizes them to something (either QUASI-ACTION! or plain ACTION!, probably plain ACTION at this point). Then the lower-level CHAIN* runs on that.

It makes sense for CHAIN to process isotopes, due to the idea of putting FUNC [] definitions in the chain anyway. So using GET-WORD!s to get isotopes out of WORD!s wins over using carets.

As for whether CHAIN* should take QUASI-ACTION!s or plain ACTION!s... it's an implementation detail. I can't tell if bending that one way or the other makes more sense. It's cleaner to write the wrapper taking quasiforms, and the array more obviously represents a collection and not code. But maybe there are other concerns. I'll wait until some other usage of CHAIN* arises to worry about it.