Seeing all ACTION!s as variadic FRAME!-makers

There was an uncomfortable period of MAKE OBJECT! where the idea of a "spec" was introduced, and the concept was that the low level object creation would be non-evaluative. This plugged some holes of how to make sure you didn't lose anything by representing null states in a block, etc.

>> make object! [[x y z] [x: 'foo z: (1 + 2)]]
== make object! [[x y z] [x: 'foo z: (1 + 2)]]

It was clunky--but pretty much no one used MAKE FUNCTION! in Rebol2 either. The thought was that it would match that...as just a low level interface to a system ability. So some higher level constructor, like CONSTRUCT, would take its place. After all--since functions had "found specs useful", objects probably would too...maybe for some attributes, or to put type specifications on the fields.

Then Generic Quoting came and Shined a Light on MAKE OBJECT!. Objects could be represented in a compatible representation with precisely one key and one value for each item, nulls represented, no spec:

>> make object! [x: lit 'foo y: null z: the (1 + 2)]
== make object! [
    x: ''foo
    y: '
    z: '(1 + 2)
 ]

So objects don't need specs. But...do ACTION!s?

Years of the clunky OBJECT! representation had passed with no firm footing on what that ill-defined spec block was for. So I promptly ditched it. And I was reminded of one of my "radical" ideas:

What if you could MAKE ACTION! on a BLOCK! just like you did an OBJECT!?

This is fully speculative, and I have no complete answer for how this would work...efficiency aside. But I'll give you a primordial sample concept:

 outer-val: <before>
 weird-add: make action! [
      this: binding of 'this ;-- a FRAME!, when executing
      arg1: variadic-take this
      typecheck [integer! decimal!] 'arg1
      arg2: variadic-take this
      typecheck [integer! decimal!] 'arg2
      sum-local: null
      (
          sum-local: arg1 + arg2
          outer-val: <after>
      )
      sum-local
 ]

Unlike how making an object runs the block you give it once and then throws it away, the concept here is that the ACTION! would do the scan once to build the frame pattern, but hang onto the array and reuse the block to fill out new frames for running the code.

If the gathering only collected the top level words, then you could do set-word assignments in any kind of inner scope or block and still reach outside the function. Then throw SET-WORD!s into the top level to get collected if you want a local binding.

"But that sounds...slow"

FUNC and FUNCTION wouldn't have to do it this way. But when asked for a rendering they could show some kind of boilerplate--as they do today for how they simulate making a unique RETURN function per instance (though they actually don't do that).

Even if you didn't use FUNC or FUNCTION but went through MAKE ACTION! and did all this the hard way, its logic could notice patterns it could optimize, like the typechecking. Or you could call functions explicitly to tell it what you meant.

Perhaps one would imagine it all getting packaged up more like:

 >> func [arg1 [integer! decimal!] arg2 [integer! decimal!] <local> sum-local] [
      sum-local: arg1 + arg2
      outer-val: <after>
      sum-local
 ]
 == make action! [
      arg1: [integer! decimal!] ;-- imagine it uses before overwriting w/fetch
      arg2: [integer! decimal!]
      sum-local: null
      func-frame-filling-helper binding of 'arg1
      (
          sum-local: arg1 + arg2
          outer-val: <after>
          sum-local
      )
 ]

In other words, there could be nothing particularly foundational about specs, or the methods or means of typechecking, or parameter types. That could all be some higher level optimization. But this would be the basic metal, you could build your own Rebol that used none of it. The only foundational thing would be top-level gathering of SET-WORD!s, and after that it's up to you.

It's just a thought experiment, but...

I think there's something here.

There would appear to be a zillion practical problems with this. Every function becoming effectively variadic breaks every function composition tool we have. You could only do it if it could trust you were using the func-frame-filling-helper protocol, and had some other advertisement. It's not clear how to communicate the refinements that were used. Some method on the frame, I guess?

But despite the seeming impracticality, there's some interesting angles on this notion of pushing off things that can't fit in the processed function specification into the body.

I mentioned that there's only 64-bits in the typeset slots--it was enough for LIT-PATH! and LIT-WORD!, but not enough for an infinity of quoting options. But it makes one wonder if the system should be blessing any particular type system so much, or if it should think of it as an optimization on something the ACTION! is free to take advantage or not.

These kinds of thoughts might suggest moving away from a spec that's somehow "hardened" by the system, but rather label it more clearly in the body by what appear to be function calls... or what could be made as actual function calls if a convention changed. When you have something like FUNC-FRAME-FILLING-HELPER cuing the process, you could imagine snagging source to one of these bodies and later pasting it into a system that used another convention by default, yet could emulate that helper...slowly.

So that's the direction I'm kind of thinking; to put the spec in the body as something that could be thought of as part of that body's natural execution under such a scheme. Even if the call that invokes it is very compact. This stops us from the 2-block system which makes it appear that the spec is somehow fundamental...the only fundamental thing in some sense is pushing a frame of appropriate size with the key names, then running code.

1 Like

This post ("Seeing all ACTION!s as variadic FRAME!-makers") has frequently crossed my mind as being pivotal in defining the language.

To draw a comparison: consider Haskell's trick that Every Haskell Function Takes Exactly One Argument. You can experience an illusion of multiple arguments by having that return another function that takes an argument, which can return another...and so on. But when you refer to something like a function's "second argument" then you can't forget what's actually going on; otherwise you're going to wind up confused by the emergent behaviors:

https://blog.tmorris.net/posts/haskell-functions-take-one-argument/

Haskell gets a lot of mileage out of this. It means their low-level power tools for hijacking and manipulating functions only have to work on one kind: the single argument function. Because there is no other kind!

Back to our world: Ren-C's "big homogenizing system concept" is definitely tied to the currency of the FRAME!, and its intrinsic duality with ACTION! It might not sound that profound, but there's a "long bet" in exposing a state which most languages consider an internal detail. And what's rigged up technically for weaving frame identity through "derelativized" homoiconic block cells is a mechanic that I'm pretty certain no other language has ever done.

Note: Although being a truly original mechanic isn't necessarily an indicator of greatness: "The reason the wheel has been invented so many times is because it's an extremely good idea. I've learned to be skeptical of things that have been invented only once."

VARARGS! and the Variadic Reckoning

Variadic functions throw a wrench into the concept that if you're looking at a series of values at a callsite, you can make a FRAME! for them. Because a variadic function doesn't decide how many arguments it wants to take until it starts running.

We've known since the beginning that variadics are voodoo, and you should avoid them if you possibly can...because they make composition a lot harder.

But variadics have been spreading into areas where I think they are the wrong intention. I was trying to design a tool called REQUOTE that would work in the following way:

>> data: [''[a b c]]

>> requote skip (first data) 2
== ''[c]

If SKIP is not willing to accept QUOTED!, then this is a concept for a tool that would:

  • make a FRAME! for SKIP from its arguments (so the slot for the thing to skip would be QUOTED!)
  • save the quote level of the first argument, and then dequote it in the frame (so now the slot of what to skip is the BLOCK! [a b c])
  • run the frame and get the result (the plain block [c])
  • return the result requoted to the level of the original input

That makes REQUOTE a pretty invasive function. It's taking over runtime control of the execution process of what follows it.

If we look at the spec for what REQUOTEs arguments would be today, it would have to be a variadic. It doesn't know how many arguments it takes until it runs. So what would it mean to MAKE FRAME! for REQUOTE in the same way that we are trying to MAKE FRAME! for SKIP? :-/

As a sample problem, we think of MY as taking the contents of the SET-WORD! on the left and "shoving" it in to act as the first parameter of the invocation on its right. So how would MY be able to do the following?

>> data: [''[a b c]]
>> item: first data

>> item: my requote skip 2  ; what would be the mechanism for this to work?
== ''[c]

When MY runs, it quotes its left argument...gets the value from the SET-WORD! Then it makes a frame for the right hand side function...fills its first parameter with what it got, and fills the rest of the frame from the remaining execution stream. Lastly it writes the result back into the SET-WORD!. But REQUOTE throws in an opaque variadic wall to this process.

Perhaps you can now see why Haskell's one-argument-per-function is so convenient. Their frames are always just one argument..."the next thing to chain in an execution". Of course this wouldn't give you any of the nice things like HELP or MAKE FRAME! which matched your cognitive level of what a multiple-argument-taking function is.

Homogenizing the Pattern

Both MY and REQUOTE are saying "I don't have a frame definition with arguments, I get my frame from what comes after me".

If the system knew that's what they were doing, then they wouldn't just have a generic VARARGS! slot filled by an arbitrary process once the function runs. They could have some number of arguments of their own...followed by the understanding that everything else is "arguments of whatever comes after me".

So to answer the question of what would it mean to f: make frame! :requote, it would mean you'd have an opportunity to fill in any of the parameters to requote itself that it didn't inherit from the function that follows it. Which is 0 in this case. But you wouldn't be able to do f on that frame, since it requires a following frame to operate on. You could reeval (make action! f) skip item 2 (and maybe there'd be a shorthand for that).

Now Back To "Seeing all ACTION!s as Variadic FRAME!-Makers"

That's some pretty important stuff and it means pushing some code around. What had once been a feature of variadics is rethought as a property the system knows about functions...it has to be aware that they intend to do surgery on the frame of the invocation after them. These functions become--in a way--analogues to Haskell's "one argument" rule, forced to obey the "one invocation" rule.

But there's other important implications to notice here, like that type checking shouldn't happen on frames until the function actually starts running!

We already get a sense of this from MAKE FRAME!. It seems like it's all right to have a temporary moment where a frame field is invalid, so long as it gets fixed before you execute:

 f: make frame! :append
 f.series: all ["abc" 1]  ; series temporarily INTEGER!, bad if run
 if integer? f.series [f.series: to text! f.series]  ; ...but we changed it
 f.value: "def"

 >> do f
 == 1def

Now think about if REQUOTE wants that frame for SKIP with the QUOTED! in it, so it can DEQUOTE it. If the frame were checked while it was being filled, there'd be an error. You don't want the check until after REQUOTE had its chance to adjust the frame.

But now we're talking about a difference of behavior. Notice how none of the below reach the PRINT:

rebol2>> append 1 (print "second argument" 2)
** Script Error: append expected series argument of type: series port

r3-alpha>> append 1 (print "second argument" 2)
** Script error: append does not allow integer! for its series argument

red>> append 1 (print "second argument" 2)
*** Script Error: append does not allow integer for its series argument

There's a slight performance advantage to typechecking argument slots while you're filling them. But when we see this model more clearly, we see the benefit to moving the type checking to being something that happens once the function start running:

>> append 1 (print "second argument" 2)
second argument
** Script error: append does not allow integer! for its series argument

THIS IS RATHER IMPORTANT - Seeing type checking as a separate phase from argument fulfillment makes a big difference. It may still be possible that you can have code controlling the procession of argument fulfillment, e.g.

>> requote skip (print "skip's first arg" item) (print "skip's second arg" 2)
requote might get a first chance to do something
skip's first arg
requote might get another chance
skip's second' arg
here we know requote needs to be able to do something with skip's frame

Inch By Inch

It would be to nice to see everything magically resolved in one crystalline flash of obviousness. But nothing with enough complexity to be worth thinking about works like that:

"The essence of architecture is the suppression of information not necessary to the task at hand, and so it is somehow fitting that the very nature of architecture is such that it never presents its whole self to us but only a facet or two at a time." link

Rippling these changes through is bound to bring more insight, so we'll have to see what that is. First up is not checking the types of arguments gathered from callsites until functions run.