R3-Alpha's CLOSURE provided two things. One was a unique identity for the words of a function's arguments and locals for each recursion. This is what I've called "specific binding" and now comes "for free" in all functions...so you don't even have to think about it. (It's not exactly free, but we can hope it will converge to "very low cost".)
So in Ren-C:
>> foo: function [x code] [
append code [print x]
if x > 0 [
probe code
do code
foo (x - 1) code
]
]
>> foo 2 []
[print x]
2
[print x print x]
2 ;-- R3-Alpha FUNCTION! got 1, only CLOSURE! got 2
1
Users can now take that for granted.
But what I want to talk about is the other emergent feature of R3-Alpha CLOSURE!. This was that if an ANY-WORD! that was bound to the arguments or locals "escaped" the lifetime of the call, that word would continue to have its value after the function ended...for as long as references to it existed.
>> f: closure [x] [return [x]]
>> b: f 10
== [x]
>> reduce b
[10]
Functions did not do this:
>> f: function [x] [return [x]]
>> b: f 10
== [x]
>> reduce b
** Script error: x word is not bound to a context
It goes without saying that the closure mechanic is going to cost more, just by the very fact that they need to hold onto the memory for what the word looks up to. But the way things work today, it doesn't just need to hold onto that cell of data...it holds onto all the args and locals of the function. (R3-Alpha was more inefficient still...it not only kept the whole frame of values alive, it made a deep copy of the function body on every invocation of that function...so that the body could be updated to refer to that "frame". Specific binding lets Ren-C dodge that bullet.)
Now and again, the "keep-things-simple" voice says that the system would be simpler and faster if all executing frames (and their frame variables) died after a function ended. If you wanted to snapshot the state of a FRAME! for debugging purposes--to look at after the function ends--you could COPY it into a heap-based object, and return that. If you really were in one of the circumstances where you wanted an arg or local's word to survive, you could manually make an object to hold just those words, and bind to that.
But @Ladislav had a compelling case:
foo: function [x] [
y: 10
return function [z] [x + y + z]
]
If x
and y
were to go bad after foo exited, the returned function would be useless.
Some new mechanics related to Move_Value() are creating possibilities for "automatic closure-i-fication", where stack cells are converted into a heap object at the moment it's noticed that a bound word is "escaping". If none escape, then everything stays on the stack.
But though you might think these kinds of escapes are rare, remember some bindings aren't even intentional. When you return a block out of a function it might just have stray bindings on words that happen to overlap with something in the binding visibility. (Which makes one wonder, when returning a BLOCK! as data, should you always UNBIND/DEEP it before returning...to scrub off any inadvertent pointers into your local state it carries? Should there be a RETURN/BOUND to avoid the scrub?) These invisible bindings would trigger the auto-closurification, on what might seem like random cases to the user.
And remember--each time a word bound to a frame escapes--we're still talking about copying all the values in the frame. (It might be possible to break this down to a smaller granularity, e.g. a PAIR!-wise binding, where what closure-i-fication does is pack each key/value into a REBSER node.)
Were the user to get involved, and specify the cases, I might suggest something a bit like this (if <HAS>
were taken to mean "a kind of per-instance static", while <STATIC>
were used for all instances):
foo: function [x <has> x2 y] [
x2: x
y: 10
return function [z] [x2 + y + z]
]
The advantages to this are that it would mean that any words that "escape" would be explicitly handled by the user, reducing the burden on the system. The entire frame would not need to be preserved, only the part of the frame which had these persistent values. The disadvantage is that it's not automatic, and other languages--even JavaScript--do it automatically.
So how do people feel on this matter? What's acceptable or unacceptable? @MarkI said at one point that he was opposed to locals and args outliving the function call because it created "garbage". Is it wise to hide the consequences from the user, and burden the system with the logic of making it automatic?