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! 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: lit (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 cueing 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.