Seeing all ACTION!s as variadic FRAME!-makers

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.