Custom Function Generator Pitfalls

Hmm, I think I see… does this mean that with-return would suddenly stop working if the body of promise-ish were to define a variable passthru?

Indeed, there would have to be a notion of the ‘currently active environment’ from which unbound words lookup their names. Apologies for not making that clear.

That being said, I hesitate to call it ‘new’ when it’s already been used in nearly every other programming language in existence. Besides, surely specifiers require the same kind of concept already?

Indeed… but it’s quite sufficient for COMPOSE to evaluate the groups within that environment. It doesn’t have to rebind anything in the process. So this:

…isn’t correct, the way I’m thinking about it: ('passthru) would still construct an unbound word, since all words would start out unbound, and continue that way until explicitly bound to something. If you want the word to be bound individually to an environment (rather than inheriting the same environment it’s evaluated in), you have to bind it yourself.

(There’s an interesting duality here, now that I think about it: all words start out unbound but can be bound to environments, whereas all blocks start out bound but can have their bindings removed.)

Yeah, you’re right. I think I had a mental blind spot there, where I thought that a quoted name can’t take any bindings at all. (Yet more Lispiness creeping into my thoughts, I suspect…!) So indeed it would need to be setify unbind name.

I don’t see how this is any different to my UNBIND. In the text you quoted, my meaning was that removing the binding of a block requires its elements to be interpreted in some other context. (Because their containing block no longer has a bound environment in which they can be looked up in!)

To be clear, let me describe how I see this being evaluated:

  • The block is bound to the current environment when created.
  • COMPOSE evaluates the groups in that environment:
    • (unbind name): looks at name, UNBINDs it and SETIFYs it, making a SET-WORD! which is not bound to any environment in particular (and thus will be looked up wherever the splice ends up going).
    • passthru is a word, which like all newly created words is unbound, and it’s not in a GROUP! so it stays the same.
    • (unbind ':return) is the same story as unbind name: it creates a GET-WORD! which is not bound to any environment.
  • The result is a block in which all three words are unbound. You might as well have written [name: passthru :return] — that would have done the same thing.
  • Then BINDABLE removes the binding from that block.
  • Eventually that block gets spliced into the promise, and it all blows up when evaluated because passthru isn’t bound to anything and the current environment doesn’t have any definition for it either.

Now that I think of it, there’s an interesting wrinkle with COMPOSE (and REDUCE, etc.) which I hadn’t fully considered:

inner: func [] [
    local: 5
    return [(local)]
]

outer: func [] [
    local: 10
    return compose inner
]

In my model, how would outer evaluate? Firstly, local would be created in outer’s environment. Then, inner is called. It would create local in its own environment, then create the BLOCK! [(local)]. This BLOCK! would be bound to the current environment, whereas the WORD! local within it starts out unbound. (I suspect that GROUP!s would have to start out unbound, too.) This returned BLOCK! is then composed… but which environment is the GROUP! evaluated in? To get the semantics we want, compose would have to enter the environment bound to the BLOCK!, so that local is looked up in inner’s environment, not outer’s. To evaluate it in outer’s environment, we would have to get rid of the binding to inner’s environment by running compose unbind inner.

Incidentally, func itself needs to do something similar, though more subtle. Clearly, each function invocation must create a new environment to run itself in. Equally clearly, the parent of that environment needs to be set correctly to ensure that the body can access global variables. But… the function could be called from anywhere, so how does func find the environment to set as parent?

The answer is quite obvious: func should use the environment bound to the BLOCK! which is its body! When the function is defined at the top level, that BLOCK! gets created in the top-level scope, and bound to the same. If it’s a nested function, same idea — the BLOCK! ends up bound to the current environment, and the function will create a new scope descending from that environment.