Taking ACTION! on "Function vs. Action"

The very noun-sounding FUNCTION is an active generator, whose word sounds like it should be the name for a variable holding a type of its value:

block: [This is a block!]
string: {This is a string!}
function: :append ;-- this is a FUNCTION!, but...
                  ;-- ...now I can't use FUNCTION to declare another!

Some have said users would have a better mental model of the generation process if it were MAKE FUNCTION! (or MAKE-FUNCTION, or FUNCTIONALIZE, or FUNCTIONIFY, or [x [integer!]] => [x + 1]), or ....

But plain old FUNCTION being a generator is rather entrenched, and there's problems with pretty much any suggestion for changing it. While things like => are good for lambdas, that's not as readable as prefix for top level declarations. The only one I could see liking would be make function! but that opens huge cans of worms.

What could change, easily, is the datatype name. Because Ren-C made the important change known-up-to-now as "OneFunction", it freed up all the words for the old subclasses (NATIVE!, ACTION!, ROUTINE!, OP!). So...

What if the "OneFunction" FUNCTION! were just called ACTION! instead?

I already approximate this with action [function!] on many parameters, just to avoid tripping over the name function, or using an ugly abbreviation like f, and func is taken too (so even fun feels sketchy).

One can argue that since FUNCTION! doesn't guarantee it acts like a "mathematical function" anyway, that the less-loaded term could be better. It is more comprehensible, I think, to say "FUNCTION is an ACTION!, whose purpose is to generate another ACTION!..." It's a shorter word. It still has "tion" in it. :slight_smile:

Backwards compatibility wouldn't be that hard, as that's just function!: :action!. To the casual user, the difference would be that all the native interfaces would call generic function parameters action [action!] (unless it were something like comparator [action!])...and type of would visibly tell them they had an ACTION!.

Seeing an apply-able value come back as ACTION! would be no big shock to Rebol2 or Red users. Their main confusion was probably before things like "hey, why did I say my argument could be a FUNCTION! but I can't pass APPEND to it..." Now it would just be "well, everything is an action!".

It's not a huge change, and it's been on the table for a while now. But no new information has come to light since the idea first appeared, around the time of OneFunction. We know more or less everything we are going to know about this.

What do people think? Is saying Rebol runs ACTIONS in CONTEXTS good, or bad?

The biggest area this probably affects is the interpreter source itself, where there's tons of references to fun or VAL_FUNC or FUNC_NUM_PARAMS. I've held off on some updates to those, which fitting patterns of other types these days would look more like:

REBCTX *ctx = VAL_CONTEXT(...); // full word used for main extractor
int n = CTX_NUM_VARS(ctx); // abbreviation used for sub-extractions

REBFUN *fun = VAL_FUNCTION(...); // ...so this should match up
int n = FUN_NUM_PARAMS(fun); // ...and so should this

FWIW: My main reluctance on this comes from feeling like the interpreter source wouldn't read as well:

REBACT *act = VAL_ACTION(...);
int n = ACT_NUM_PARAMS(act);

Though historically, such things start seeming normal pretty quickly. For instance: I thought CTX was too "ugly" an abbreviation compared to REBCON, but I now think it's "obviously better".

It would also help distinguish between C functions (CFUNC) and Rebol actions...which would add value.

Or am I wrong, is there still some un-thought-of suggestion or issue to be discussed here? Can we commit to a decision now, or not!?

Note that Red/Rebol2/R3-Alpha say the following:

>> function? :append
== false ;-- wait, what?!

>> type? :append
== action! ;-- er... that's not exactly... clarifying

Making action? work for all Rebol-invokable-things has the advantages I list. Quick re-summary:

  • Has existing precedent as a word used to describe invokable things.
  • Shorter name than FUNCTION!
  • Helps disambiguate in the interpreter source itself, that if a variable holds a "function" you're talking about C executable code, and if you're holding an "action" you're talking about Rebol executable code.
  • "Rebol: Actions in Contexts" sounds differentiating...as opposed to "Rebol: Functions in Objects" which does not really command much attention.
  • Less loaded term than function, which has a more strict mathematical meaning, regarding consistency of same output for same inputs...no side effects...etc.

And of course, the reason that motivated it all: you can call a variable action to hold an action! without destroying the ability to make new ones, the way making a variable function to hold a function! would.

So I'm going to change "OneFunction" to be OneAction. The list of PROs is long, there aren't many cons (considering that historically was (function? :append) = false, it can't be considered a "step backwards" in any way). I've long known it's better than FUNCTION!, my only worry has been that some other idea or paradigm is even better, which has made me hold off. But waiting longer has ceased to make sense.

After four years of relative peace in naming, there's a new issue...

Whatever we call the "thing-that-is-now-ACTION!", it will come in two forms...

The non-quoted form will not execute without an apply. Here is an example of a function whose parameter takes just a plain action!, and so you can run it.

 foo: func [act [action!]] [
     if action? act [
         print "This will work even though you didn't say :act"
    ]
    apply act ["one" #way 2 <run>]  ; again, no :act needed
    act/ ["another" #way 2 <run>]
 ]

The isotopic form will run when referenced through a WORD! As with history, you'll need to disarm it somehow or another to get at it:

>> :append
== ~#[action! [series value /dup /part]]~  ; isotope

 >> ^append
 == ~#[action! series value /dup /part]]~

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

How Might This Shuffle Naming?

An isotopic group is called a "splice".

An isotopic block is called a "pack".

In the prototype implementation, I'm calling isotopic actions "activations". But that's a little weird.

An alternate conception might be that the inert thing is a FUNCTION! and once it's isotopic it would be called an ACTION!.

Or we just call them "action isotopes". Maybe that's fine, in the sense that there's not as much of a difference in semantics to convey as with packs and splices.

Thoughts?

2 Likes

function / action
(inert) action / (active) action
or action and action isotope are all fine with me.

routine! seems to be unused.

1 Like

Something sort of clicked around in my head when I wondered...

What if ACTION!s were just isotopic FRAME!s?

:exploding_head:

It looks... good.

foo: function [frame [frame!]] [if true [run frame arg1 arg2]]

bar: function [action [action?]] [if true [action arg1 arg2]]

baz: function [frame [frame!]] [let action: runs frame, action arg1 arg2]
  • It's clear that FRAME won't do anything when you reference it from a word

  • It's clear that ACTION will run when you reference it from a word

  • It leaves the word FUNCTION free (which over the ages has been determined non-negotiable as semantically meaning MAKE-FUNCTION)

  • Mutating between a frame and an action is easy and obvious

Can It Work?

ACTION!s and FRAME!s actually line up very closely. They each have as many slots as a function has parameters and locals. But they use these slots differently.

ACTION!s hold type checking information in the slots for arguments, and the frames point to the actions they were made from to get this information. This is because the FRAME! slots need to hold actual argument values. When you DO a FRAME!, it follows the link it kept to its action to do the type checking.

Yet in some sense, this is an implementation detail. There's not really a reason why every frame couldn't have made its own copy of the parameter descriptions...and no reason why the original frame couldn't have empty argument slots. You could just say that immutable frames "throw away" their argument slots...and copied frames inherit their parameter lists.

The differences could be glossed over by magic under the hood, retaining the current efficiency. Basically the FRAME! datatype cell would just be able to hold the guts of what an ACTION! cell currently holds as a potential alternative to what it holds currently, and expose those guts as if it were an immutable frame.

Usage Difference

As currently designed, the system would have a bit of a chicken-and-egg problem... you need an ACTION! to make a FRAME!... e.g. make frame! :append

But if APPEND was an isotopic frame, and the MAKE FRAME! in this case was just doing basically copy noquote :append ?

In such a world, you'd presumably want to PROTECT or CONST the frame behind APPEND, or you'd wind up with it being too easy to change functions:

>> f: noquote :append
>> f.value: [d e]  ; is actually specializing APPEND!

>> block: [a b c]

>> append block [x y z]
== [x y z]

>> block
== [a b c [d e]]

But that's fixable with a trustworthy system of immutability (which Ren-C has).

A plain FRAME! that was found inline would have to execute a copy of itself, the way a plain action does today, so this wouldn't do what you likely expected:

make object! compose [f: (frame)]  ; would execute the frame

This isn't exactly a dealbreaker, because it's par for the course:

make object! compose [w: (word)]  ; would evaluate the word

So you'd have the usual tools available, same as for other types:

make object! compose [f: '(frame)]
make object! compose [f: (quote frame)]

make object! compose [f: ^(frame)]
make object! compose [f: (meta frame)]

Trying This Would Raise Many Questions

It's a bizarre thing to do if the only reason is to sift out the naming for a word-inactive vs. word-active function invocation. Though it does seem to solve it nicely: "ACTION!s are isotopic FRAME!s which DO (a COPY of) themselves if referenced via WORD!"

But there are other advantages to reducing the total number of exposed types.

I can think of one advantage already...which is the PARSERs passed to combinators in UPARSE. Today they are ACTION!s formed from specialized frames, to make them easy to call. But I noticed that passing them as FRAME!s would be cheaper...as well as permit the choice to execute the frame directly without making a copy, if you were going to call it only once.

There's a disadvantage I can think of too... which is loss of flexibility for interpretation of field selection out of ACTION!s. Under this model, append.value should be an unset value...as APPEND is an immutable "archetype" frame. I'd hoped that the action.xxx space could be taken for special fields that were associated with the functions, things like append.help for example would be picking from an object associated with APPEND, as opposed to picking useless fields out of the unspecialized frame that APPEND represents.

(Of course, Ren-C has other tricks up its sleeve, and might be able to do this with something like append..help for instance. Today this information is accessed through the peculiarly-named adjunct of)

Anyway, still just a thought. But maybe a unifying thought that's good enough to see what came up trying to implement it.

1 Like

This may deserve the understatement of the year award!

:trophy:

Nevertheless, after many hours of hacking around, I've gotten it to sort of work!!

Overall Use Impressions Are Mostly Positive :heavy_plus_sign:

Behold:

>> f: make frame! :append
== make frame! [
    series: ~
    value: ~
    part: ~
    dup: ~
    line: ~
]

>> f.value: 100

>> ap100: isotopic f  ; synonymous in this case with `runs f`
== ~make frame! [
    series: ~
    value: 100
    part: ~
    dup: ~
    line: ~
]~  ; isotope

>> ap100 [a b c]
== [a b c 100]

That's extremely cool. Something to really like about it is how it demystifies aspects of how actions are manufactured, making it easier for people to write their own SPECIALIZE-like and APPLY-like tools in usermode...without relying on voodoo inside SPECIALIZE or APPLY natives!

Or so it would seem.. BUT... a bunch of broken cases draws attention to some of the "hidden" logic that makes specialization not so simple.

For instance: if you create functions like adp: :append/dup/part, then it loses the /DUP and /PART refinements and instead always takes them, having 4 normal parameters. And if you said instead apd: :append/part/dup then APD would also take 4 normal parameters, but with the PART and DUP in a reversed order from ADP. The mechanics which encode this aren't visible in the FRAME!... they're little chains of hidden state. And if you MAKE FRAME! on ADP or APD, then set PART: or DUP: then those chains currently need to be fixed up to know that the fields are specialized out and should no longer be mentioned.

All that has historically been magically covered by SPECIALIZE, which was creating an opaque ACTION! where internally all the bookkeeping was handled for you. If you are just poking values into a FRAME! and then off the cuff running it--expecting it to act partially specialized--then the current data structures can get out of sync and crash.

It is clearly looking like a massive amount of work to rethink this. But I believe the product of that rethinking would lead to more complete and coherent model of FRAME!, making it a more powerful user-exposed facility for building actions.

So I think I'm sold on this. That said, here's some more to say.

Plain FRAME!s In BLOCK!s Execute

This throws a bit of a curveball into how I'd historically segmented FRAME! and ACTION! in my mind...where if you COMPOSE a FRAME! into a block, that it is active:

>> frame: make frame! :add

>> do compose [(frame) 10 20]
== 30

But if we didn't choose this semantic, then since you can't put isotopes themselves into blocks... all invocations of functions would have to be triggered by word references to isotopic frames.

e.g. if we left frames inert when encountered directly by the evaluator, you'd always need to go through words-pointing-at-isotopes like RUN or APPLY to get execution:

>> do compose [(frame) 10 20]
== 20  ; let's imagine this falls out, last of three inert things

>> do compose [run (frame) 10 20]
== 30

But then this costs a WORD! lookup and an extra function call, which strikes me as unacceptable overhead to require in order to execute a function. It's almost as bad as requiring a declaration.

>> do compose [let act: ~(frame)~ act 10 20]  ; just added: quasicompose!
== 30

It could perhaps be done through some kind of terminal-slash semantic:

>> do compose [(frame)/ 10 20]
== 30

But that's contentious with my terminal-slash APPLY semantics. And since frames can't be put in paths directly, it would have to be put in a group in a terminal-slash path, so more cost and complexity added there.

This is all a rehash of things I dealt with back when isotopic actions were first introduced (...and you can read through the stumbling around of that era...). I was a bit uncomfortable that the "inert" form of ACTION! would execute when encountered literally in block evaluation. Sure, it was inert when fetched via word, but was very alive if you composed it in.

Eventually I decided that as long as the key behavior of being fetched via word being inert was met, the semantics when seen literally during a block evaluation didn't matter. After all, WORD!s require a similar caution to be quoted.

This idea of FRAME! executing seems more unsettling, considering how frames have been inert like objects for so long. But when you start thinking that FRAME! has execution potential... like GROUP! and WORD! and GET-BLOCK!, etc... then imagining it executing isn't as much of a stretch. Especially under a new model where it's only an isotope away from running if fetched via word.

Incidentally, if you read the old isotope stumbling around thread, I did mention the idea of isotopic frames executing, July 2022:

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

How big of an issue this really is depends on how often people compose FRAME! into blocks and execute them when they -DON'T- intend to run them. The main case to worry about is the MAKE OBJECT COMPOSE [...] scenario, but I've pointed out how you've got to fret over GROUP! and WORD! and PATH! and any other evaluator-active types.

I think it's acceptable to have FRAME! be another evaluator-active type. And people who aren't used to it being anything else won't see it through the same lens I'm seeing it now. Maybe they'll think "hey, it's one isotope away from being WORD!-active, why wouldn't it run?"

MESSY MOLDING

This isn't an entirely new problem, as historical Rebol always had a problem rendering functions.

But Ren-C had been molding ACTION!s compactly as just the cached name of the action (from the last SET-WORD! it was used with), along with a compact list of the parameters. However, FRAME!s were molded like ordinary objects.

Now it's a bit of a quandary to decide how to do the rendering. We could say that if the console is showing you an isotopic FRAME!, then it does a light rendering looking like a historical action... but if the frame is normal, then you get the fields. Which is something I'm probably going to do shortly. If QUASI-FRAME!s were given this treatment as well, most actions would appear compact.

Anyway, like I say, not particularly a new problem. The limits of a text console really bite us here...maybe the web REPL can do better.

MAKE FRAME! ACTION OF BINDING OF

Consider this case of leveraging the linked relationship between an action and a frame:

foo: func [x] [
   f: make frame! action of binding of 'return
   probe f
]

Now let's say you call it with foo 1020. I would expect f.x to be unset, because what we effectively asked for was to make frame! :foo

But if there was no such question as ACTION OF, and you instead wrote:

foo: func [x] [
   f: make frame! binding of 'return
   probe f
]

This would mean MAKE FRAME! is taking a FRAME! as input, and in that input frame, X is 1020.

Is it clearly implied that all the variables should be zeroed out?

If this were the case, we'd expect the same from MAKE OBJECT! of an existing object...to get the fields and not the values:

 >> obj: make object! [x: 10 y: 20]

 >> obj2: make object! obj
 == make object! [
        x: ~
        y: ~
    ]

That's not how Red works...it seems to be a synonym for COPY OBJECT. FWIW, neither Rebol2 nor R3-Alpha permit it.

Debugger Queries: "Find Any Calls to Action X On Stack"

There was an old feature in the now-very-defunct-debugger, which allowed you to look above you for stack levels that were "instances of a specific function".

This would be defeated by specialization. So let's say you made a specialization of append that fixed the value at 5:

ap5: specialize :append [value: 5]

Calls to this function would not match as calls to APPEND. They're calls to ap5--and the fact that there's a relationship between AP5 and APPEND would be wiped out.

But now, it's sort of defeated in all cases... because there's not a concept of ACTION OF.

However, maybe this is something that can be worked through in a better way. A frame with fields filled in, turned into an isotope, is enough to be executable. So if you want to make a specialization now, you don't go through the opaque step of forming a new action. The only time you create opaque levels is if you do something like an ENCLOSE or ADAPT.

So there could be something like an "action" in the sense of identity, that you could ask for the "outermost function generator in charge". It would give the same answer for :append and :ap5 in the new model. It's kind of like an answer to BODY-OF.

HIJACK Semantics

HIJACK is one of those things in the system that really throws a wrench into just about everything. The design is about as clean and clever as I think I can make it, but every time I meddle in the frame and action mechanics something related to HIJACK trips up.

Unfortunately we are now completely dependent on it working, as it's how things like the ReplPad hook into providing its own handling for PRINT and such.

In any case, the granularity for HIJACK was on ACTION!s... not FRAME!s. So with the two unified into one umbrella idea of a FRAME!, we're only able to hijack "legacy-action-like" FRAME!s and not arbitrary "context-like" FRAME!s. We can offer an operator to turn a context-like frame into a legacy action frame (you could just ADAPT it with an empty block and get one)...and you could then hijack the result of that. But you can't specifically hijack the context-like FRAME!s.

It may be that this can be solved with some kind of magic, but I haven't wrapped my head around it, and there are a zillion other things to be solved first. Suffice to say that the black art of hijacking just got a little darker.

This Is The Tip Of The Iceberg, BUT...

...I still think it's a winner. Again... when I look at this, I feel satisfied... the annoyance of ACTION! vs. ACTIVATION! is all washed away:

foo: function [frame [frame!]] [if true [run frame arg1 arg2]]

bar: function [action [action?]] [if true [action arg1 arg2]]

baz: function [frame [frame!]] [let action: runs frame, action arg1 arg2]

Conceptually, getting to a point where "ACTION" and FRAME! are seen as two sides of the same coin seems coherent: a frame needs to encompass everything you need to know about an action in suspended animation. I may not be able to get that fully sorted right now, but it points a compass toward how to make decisions in the future.

But it's likely going to force some big internal name changes. I'd been calling the stack levels internal to the system juggled by the stackless trampoline "Frames"...because whenever you extracted an object-like thing from a FRAME! you'd get it as a "Context". But now, the design is necessitating a fusion of the guts of what was an "Action" with a "Context" to make a new abstraction...and it seems fated that this abstraction needs to be called a "Frame".

Right now, I'm reusing "Action" as the name of the fused entity...and saying that when it's "legacy-action-like" it's a "Phase", but when it's context like it's an "Exemplar". But naming disconnects like this create a lot of problems.

So I'll likely rename the internal trampoline entities "Level", and give "Frame" to be what is held by FRAME! values. There's actually some good arguments for why Level is different from what you'd think of as a traditional "stack frame". I don't mind it, but it's just a lot of churn...(oh, why do we store source code in ASCII still?)

1 Like