(I talked about this in chat, but thought perhaps here would be a better place to record the thoughts.)
In thinking about this question about reuse of parse rules, we confront Rebol’s practice where BLOCK! is used as a unit of currency…in places where other composition-based languages would demand a function with explicit arguments. The trick in Rebol’s playbook is that each level of recursion doesn’t force conventional “parameterization”, your intent can be conveyed with a “rich” data structure which gets interpreted by the evaluator…either fully (as with the body of a loop) or partially (as with a GROUP! in a parse rule).
This gets you unstuck from a rigid model of information exchange (as
f(x(y,z(a,b,c))) indefinitely). But if you use a BLOCK!, the absence of parameterization via the “call stack” (let’s lump in parse rule embedding as its own kind of a “call stack”), puts a lot of pressure on binding to accomplish composability. And binding, even ignoring its costs to do correctly, is a bit of a Rube-Goldberg black art.
While contemplating the benefits of the loose and unusual idea of “part code, part data” composition-via-data-structure, it’s worth noticing that composition via ordinary functions has a long history and can do pretty much anything. We might note that in the extremes of no access to mutable global state–a la Haskell–you can (and probably should) write IF in userspace too:
if' :: Bool -> a -> a -> a
if' True x _ = x
if' False _ y = y
Their IF is handcuffed far more than a shared parse rule is, yet they have ways of escaping constraints. The Monad jumps in as a boundary-breaking concept, exempting one from certain rules in order to balance an otherwise imbalanced equation…all in the service of composability. Interestingly, if not coincidentally, one of the two involved operators is called bind (>>=):
(Recommended Viewing: Don’t Fear The Monad…not that you need to understand monads to understand what I’m trying to get at here, but everyone with an interest in software paradigms in this day and age would probably enjoy knowing at least a little about them.)
It seems to me that for Rebol to go further, it needs its own cross-cutting boundary-breaking concept…something that’s equally applicable to PARSE as it is to DO or your own dialects. And in an imperative language, the only place I can see missing leverage coming from is the “stack” (again using my extended definition of “stack”, where recursion in PARSE rules is effectively a call stack).
@rgchris’s direction of solution to the parse reuse problem is trying to mine that “call-stack” sensitivity with THROW and CATCH; where a rule achieves generality by deferring to the context of invocation. That would be a very PARSE-specific mechanism. But I’ve been talking for a while about something called virtual binding which is a system-wide way that parameters might follow a block around.
The good news is: Ren-C has a fairly solid technical basis to attack scenarios in this vein. Where Red’s BLOCK! cell has a “reserved for future use” slot, Ren-C has leveraged that pointer-sized slot to build up a good model for beaming contextual information down through blocks. When a cell is pulled out of an array, it is considered “relative until it is combined with a specifier”…and there’s a whole tricky type-checked bookkeeping making sure every relative cell undergoes that specification process before any words can be looked up. (A function FRAME! is one kind of specifier, and it’s the reason why recursions in Ren-C have unique lookups for the same word based on the recursion, while Red’s hands are tied here and it can only resolve words by looking at the C call stack.)
The bad news is: I’m not sure exactly how to use this to practical effect. The best idea I had so far was to say that REFINEMENT! would be a way of accessing contexts which had been augmented via the call stack… so one would be able to have one block that without a BIND could actually search linked attribute space based on who did the DO CODE.
>> code: [print /foo]
>> do in [foo: 10] code
>> do in [foo: 20] code
It’s an idea that can be accomplished with no binding or modifications involved. All because that out of band “specifier” threads through the execution path, and so for this example it’s IN that tweaks that specifier to add some information (in this case an OBJECT! created from the block contexts).
Just to reiterate the good news: this mechanic is already solved…when a “cell” is picked out of an array, it is considered to be “unspecified”. The only way it becomes a “specified” value (one which can resolve to a variable in a context) is if something threaded through the call stack gets recombined with it.
The underlying mechanic is something that has been working for well over a year now, and is pretty well understood, and checked to the point that I’m confident it works. The question is how to use this in practice for the practical problems we are seeing. I feel like if we get too theoretical about how parameters get synthesized or inherited, it starts seeming like attribute grammars, and that gives me headaches. So perhaps we can focus on real examples and see if there’s any good-enough tricks to pull.