Context-Switching: Changing The Binding Environment

Initially, I resisted the idea of constructs that were sensitive to the binding in effect at the callsite.

Eventually I decided that COMPOSE needed to be such a construct.

It may seem that was in order to make it possible to do interpolation on strings, without having to say that strings themselves carry binding:

>> word: "this"

>> compose "Wanted (word) to work"
== "Wanted this to work"

But it turned out there was another motivation: that as unbound material was becoming an important currency, you frequently wouldn't even want BLOCK!s and such to carry bindings, you'd just want to compose inside them:

>> compose '[raw material (10 + 20) but you need to find plus]
== [raw material 30 but you need to find plus]  ; unbound

This realization--that it's very frequent to want the binding of the templated code to be different from the evaluations you embed inside it--pushed me over the edge to believing COMPOSE had to be the kind of construct that could be aware of the current binding context at the callsite. It just happened to also facilitate easier string interpolation, which sealed the deal.

But then, how do you slipstream another context into the mix?

Well, you could say there's a COMPOSE:INSIDE or something like that (let's imagine we're using the new proposed ability where :INSIDE can put its argument before and not after)...

>> obj: make object! [add: subtract/]

>> compose:inside obj '[foo (add 10 20) bar]
== [foo -10 bar]  ; unbound

That's one idea. But it means that every function sensing the binding context at the callsite would need such a refinement.

You might think of manually binding and evaluate your expression from scratch:

>> code: bind obj '[compose '[foo (add 10 20) bar]]
== [compose '[foo (add 10 20) bar]]  ; bound

>> eval code
** PANIC: compose is unbound

Ooops. Can't find COMPOSE now. You'd need to get more clever than that, injecting the compose from outside into the code block to act as the operation:

>> code: bind obj compose '[($compose) '[foo (add 10 20) bar]]
== [compose '[foo (add 10 20) bar]]

>> eval code
== [foo -10 bar]  ; unbound

Awkward as that is, it's actually rather cool that "binding science" is starting to actually have answers for these kinds of things. :smiling_face_with_sunglasses:

But clearly there should be an easier way of doing that. One idea would be to say that it's an argument to APPLY:

>> apply:inside obj compose/ ['[foo (add 10 20) bar]]
== [foo -10 bar]  ; unbound

That seems to give you the right granularity for the intent, and isn't a terrible idea.

Wild Brainstorm: What if "INSIDE" is like a REFRAMER

Remember that reframers work like:

>> two-times: reframer func [f [frame!]] [eval f, eval f]

>> two-times append [a b c] <d>
== [a b c <d> <d>]

Let's forget historical meanings of INSIDE, and use our imagination with:

>> inside obj compose '[foo (add 10 20) bar]
== [foo -10 bar]  ; unbound

That breaks expectations a bit (as reframers do), but it's certainly less awkward compared with:

>> apply:inside obj compose/ ['[foo (add 10 20) bar]]
== [foo -10 bar]  ; unbound

If you don't mind getting even weirder, INSIDE could be infix like the // operator, so COMPOSE would come first:

>> compose inside obj '[foo (add 10 20) bar]
== [foo -10 bar]  ; unbound

But this would require people to read that as:

compose-inside obj '[foo (add 10 20) bar]

Not:

compose (inside obj '[foo (add 10 20) bar])

Too weird, or cool?

Other Ideas: EVAL Auto-Escaping

I pointed out the clumsiness of:

>> eval bind obj compose '[($compose) '[foo (add 10 20) bar]]
== [foo -10 bar]  ; unbound

What if there was something less clumsy encapsulating this idea, such as:

>> weirdeval bind obj '[/compose '[foo (add 10 20) bar]]
== [foo -10 bar]  ; unbound

Maybe any function calls denoted with /XXX would be assumed to use the binding of the outer scope?

I dunno. There are ideas here. But I think the key is I don't think every callsite-context-dependent function should need to have an :INSIDE refinement.

I bought into this concept a bit more, with things like RETURN being a "generic return" which would look up RETURN* in the "current binding environment" and run it.

This gave the ability to override the behavior of "RETURN in general"...becoming able to affect multiple instances of definitional return.

That works well with things like ADAPT, because the ADAPT will fall through and run the function from its initial context:

 return: adapt return/ [value: value * 10]
 foo: func [] [return 102]
 bar: func [] [return 3.04]
 foo  ; gives 1020
 bar  ; gives 304.0

But I realized that this can become broken if you use a function composition tool that makes "new callsites", hence new places to do lookups from.

Consider ENCLOSE:

 return: enclose return/ lambda [f] [f.value: f.value * 10, eval f]
 foo: func [] [return 102]
 bar: func [] [return 3.04]
 foo  ; error: no generator providing RETURN*
 bar  ; error: no generator providing RETURN*

The problem here is that the EVAL F is acting like you wrote the RETURN inside the body of the lambda. The FRAME! doesn't (currently) have anywhere to store the binding environment it was called with.

So unless FRAME! develops a mechanism to snapshot these bindings implicitly, you'd have to be involved in consciously moving the binding environment from the "encloser" to the "enclosee".

Let's say there was some kind of callsite-context tool for that:

 return: enclose return/ lambda [f <callsite-context> ctx] [
    f.value: f.value * 10
    inside ctx eval f
 ]

But it occurs to me that another way of looking at this would be if we could BIND a frame itself:

 return: enclose return/ lambda [f <callsite-context> ctx] [
    f.value: f.value * 10
    eval bind ctx f
 ]

But there's currenly no space in a FRAME! Cell for a binding. The 4 slots are:

  • The header
  • VarList of data for the frame (or parameter definitions, if archetypal)
  • The "Lens" which I've explained elsewhere
  • The "Coupling"...which is what connects methods to their objects, or something like RETURN* with the function it returns from

There's some strain here on the difference between "coupling" and "binding". And maybe the two concepts can be fused... and perhaps something like return/ would give you back the global RETURN but with a binding in the current environment. That may be a better default...with get $return not binding.

Being able to BIND a function does look like it may have meaning in a way that I hadn't anticipated in the pre-virtual-binding era... I did split it out into this "coupling" idea but it had originally been fused with binding. Maybe they really are the same concept after all. :thinking: