Using LAMBDA/FUNC as a Generalized Binding Tool

I wrote up a bit on how COLLECT and KEEP are implemented, where the "keeper" function is passed as an argument named KEEP to a function that has been built out of the collect body.

Historical Rebol's idea of binding in functions is that regardless of where the contents of a function body came from, any arguments or locals will be deeply bound... overriding previous bindings of the word.

So even in examples of a deep composition like this...

 keep: does [print "Outer KEEP"]
 code: [keep <example>]
 collect compose/deep [
     repeat 1 [
         repeat 1 [
             repeat 1 (code)
         ]
     ]
 ]

...the KEEP composed into the collect body will find the KEEPER supplied by the COLLECT, not the function defined on the top line.

Questions raise about how it is possible to know that this is what was desired. Preserving the original KEEP's meaning is another valid intent... and arguably the more conservative intent for composition.

Biasing Toward Binding Preservation

@bradrn has suggested that if a block is bound, then KEEP would only be overridden in the topmost level. The reason it would work would be due to a single-step of surgery done to merge the function's variables into the environment of the lambda's body block:

 keep: does [print "Outer KEEP"]
 code: [keep <example>]
 collect code  ; would generate [<example>] block

The proposal is that it only works because the KEEP inside of CODE is actually left unbound. The CODE block itself has a binding on its tip, bestowed upon it when the evaluator passed over it...but this did not descend deeply.

You'd get a different outcome if you bound KEEP itself. Hypothetical code that would do that:

 keep: does [print "Outer KEEP"]
 code: compose [(bind-to-current 'keep) <example>]
 collect code  ; would print "Outer KEEP"

Also, the evaluator would defer to the bindings of blocks or groups found deeper than the topmost level. They would not get the override:

 keep: does [print "Outer KEEP"]
 code: [keep <example>]
 collect compose [(as group! code)]  ; would print "Outer KEEP"

If you wanted to get already bound items to see the COLLECT's KEEP, you would need to "punch holes" in the binding. For the moment, let's call the hole-punching construct UNUSE:

 keep: does [print "Outer KEEP"]
 code: [keep <example>]
 collect compose [(unuse [keep] as group! code)]  ; gathers [<example>]

Why One Level of Binding, Not Zero?

It might seem a little random to "override KEEP for one level of depth". Why not always need UNUSE if working with bound material?

 keep: does [print "Outer KEEP"]
 code: [keep <example>]
 collect code  ; would print "Outer KEEP"

 keep: does [print "Outer KEEP"]
 code: [keep <example>]
 collect unuse [keep] code  ; would generate [<example>] block

The problem here is that the operating suggestion is that code is unbound by default, but blocks capture a binding in their environment under evaluation (again, just at the tip). So ordinarily literal code like the following wouldn't work (nor would a lot of other things, like basic function definitions):

 keep: does [print "Outer KEEP"]
 collect [keep <example>]  ; would print "Outer KEEP"

So the concept of things like COLLECT (via LAMBDA) or FOR-EACH performing "environment surgery" at only the topmost level of a received block is mechanically tied to the notion of a common currency of blocks that have bindings at the tip, but are unbound otherwise. These would be the most frequently dealt with blocks that are received. The burden passes to those doing COMPOSE-like operations to be explicit about what holes they punch in the code they use, otherwise the default assumption is that all bindings will be interpreted as is.

In this line of thinking it isn't the shallow one-level-step of merging a tip-binding that is "weird", it's the person composing in already-bound code that wants to give up some of its bindings. So the burden is shifted to the composer.

An Alternative To UNUSE of Bound Code: Unbound Code

Today all arrays get bound (and incorporate "overbind" instructions, like LETs). But another proposal from @bradrn is that quoted code not add any binding under evaluation. As source code starts out unbound, this would make it easier to create a code fragment that picks up all of its meaning as if it had been written where it is being composed:

 keep: does [print "Outer KEEP"]
 code: '[keep <example>]  ; bypasses evaluator binding "tip" of block
 collect compose/deep [
     repeat 1 [
         repeat 1 [
             repeat 1 (code)
         ]
     ]
 ]  ; would generate [<example] block

This wouldn't be appropriate for something like passing a block of code to a function generator in a library. But it could let some situations avoid needing to do an annoying amount of UNUSE-ing.

Note that this would apply to all quoted things, including quoted WORD!s. This would make it harder to accidentally put stray bindings into things, meaning you'd inherit the default interpretation of a block:

 keep: does [print "Outer KEEP"]
 code: []  ; tip binds current evaluative context
 collect [
     append code 'keep  ; unbound word inserted into bound block
     append code <example>
     do code  ; would print "Outer Keep"
 ]

This would reduce stray bindings in the system significantly, which is a good thing.

1 Like

Echoing this post... to make clear the implications of "tip-binding" under evaluation...

...in this model, the BLOCK! stored in the CODE variable is what has its tip bound (stored in the block specifier), not the BLOCK! literal that was evaluated to make it (that block's specifier is presumably null):

e.g.:

>> stuff: [
       keep: does [print "Outer KEEP"]
       code: [keep <example>]
       collect code
   ]

>> do stuff
== [<example>]

>> fifth stuff
== [keep <example>]

>> do fifth stuff
** Error: KEEP is unbound

>> do code
Outer KEEP
== <example>

Here the tip binding of the block stored in STUFF would be to the console's context, received when the evaluator processed the block, which inherits from LIB.

The permissive allowance of KEEP and CODE to be assigned via SET-WORD! to that global context here vs. generate an error would mirror today's behavior... basically saying that unbounds default to trying to pop out of the lowest-level context in the "chain" (not counting LIB). Though I've suggested that might need an analogue to LET that I've called EMERGE when in "strict mode", and that strict mode is probably a better default.

2 Likes

This is a really excellent summary of what I’m proposing; thank you for writing it up!

One minor note: I don’t love the term ‘tip-binding’, because it doesn’t seem quite right to me to call the outermost block the ‘tip’. But that’s a very minor and insignificant objection.

Also, here:

Quoting to unbind is not really an ‘alternative’ choice… rather, it’s a natural consequence of how this system should be implemented. It happens for the same reason that fifth stuff in your last example is unbound.

When I said "an alternative choice" I meant "an alternative choice to using UNUSE" (clarified) e.g. the choice of using unbound code.

But as for the question of whether quoted code must be unbound... it could also be that all things are bound under evaluation, quoted or not, but you can UNBIND them.

You could write:

keep: does [print "Outer KEEP"]
code: unbind '(keep <example>)
collect compose/deep [
    repeat 1 [
        repeat 1 [
            repeat 1 [(code)]
        ]
    ]
 ]  ; would generate [<example>] block

It would be easier to make bound non-inert arrays (GROUP!, SET-BLOCK!s, etc)... otherwise you'd need something like as group! [some block] or bind-to-current '(some group).

This would let lambda [x] [get 'x] work, and thus be more compatible with today's expectations.

But as I said, I like the idea that what happens when a quote level is dropped has parity with what happens when a variable is fetched.

And on that point, quoted things aren't unbound from evaluation... they just don't change the binding from what they "contain" when evaluated.

>> original: [print "Hi"]
== [print "Hi"]

>> do original
Hi

>> generated: do compose [(quote original)]  ; or ['(original)]
== [print "Hi"]

>> do generated
Hi

For me, this is what really drives the "don't bind" rule for quotes... because it needs to be "don't interfere" for cases that don't have a WORD! reference available to shield from the evaluator's influence. Then it just so happens that all scanned material would start off unbound, so quoted source material would evaluate to being unbound.

Anyway, reducing the amount of stray bindings sounds very appealing, this feels like a good direction to be taking.

2 Likes

Indeed, this is precisely what I meant too. When the evaluator encounters a quoted value, I feel it should only reduce the quoting level, and not modify the value in any way. To do otherwise would just confuse the idea of quoting as something which ‘protects’ a value.

This is also why I placed so much emphasis on creation vs evaluation. At first I was thinking that blocks could be bound as soon as they’re created by the scanner… but then I realised that makes no sense. So blocks must be created unbound, and that has the immediate side-effect of making quoted blocks evaluate to being unbound. (That’s why I called it ‘a natural consequence of how this system should be implemented’ above.)

2 Likes