R2 vs. R3 Contexts
R2 vs. R3 contexts by Brian Hawley
This is a Language Recipe
Add your own Cookbook Recipe
Let's think of a context as a being block of words and an associated block of values (close enough for our purposes). You can only bind a word to a context if it is in that word block.
REBOL 2 Contexts
R2 just has one kind of context (two if you include the autoexpand context used by system/words). Functions, objects and USE all use this type of context, and words are bound to this kind of context.
A function! in R2 gets one of these contexts and the words in its code block are bound to it. This context gets reused every time the function is called - this is the main reason R2 could never be thread-safe. When the function returns, any values still assigned to the words in the function stay assigned; the values are only reset when the function is called again. When the function is called recursively, the value block of the context is pushed on a stack but the rest of the context is reused.
The only difference between object! contexts and function! or USE contexts is that the word 'self is added to them.
REBOL 3 Functions
In R3, there is at least one more context type: stack-relative, which is used by the function! type. All of the words bound to a function! resolve their bindings relative to the context in the executing stack frame, rather than directly. When a function starts it allocates a context on the stack; when it ends it deallocates the context. This means that the real context of the words bound to a function context only exists while the function is running - there is no point to referring to these words outside of the function.
Although the stack indirection slows down word references around 28%, it speeds up function calls and makes recursion and multitasking easier. As a bonus, references to any values still assigned to the function's words go away when it returns - no longer a potential potential memory leak.
This slowdown in function word references is the reason that R2-style Rebcode doesn't make as much sense to add to R3 without some changes to its semantic model to make up for the difference in speed.
REBOL 3 Closures
A closure! in R3 is like a USE call in R2 (USE in R3 is implemented using closure!). When the closure starts, it creates a new context and bind/copy's the code block to it. Words bound to this new context can then be returned from the closure or assigned as word values, and their bindings will still be valid. Closures are useful for generators, partial application and other functional programming tricks.
When called, a closure repeats most of the overhead of creating the closure in the first place, so you should generally use functions instead unless you need to use the closure's words after it returns. On the bright side, words bound to a closure context are direct-bound so they are just as fast as object! word references.
If you bind an external word to a closure's context before the closure is called, that binding will not be updated when the closure is called unless the word is in the closure's code block, not an easy task to accomplish from the outside. Many of the in-place code modification tricks used in R2 don't work in R3 for security reasons, so it's best to expect that any such code will need to be rewritten anyways.
REBOL 3 Objects
In R3, object! contexts are like the R2 system/words context: They can expand, but not shrink; closure! and function! contexts can't expand though. Object words are direct-bound. There will be further changes to the way object! contexts work as the semantics of objects are refined, and thread-safety is considered. Module contexts will behave the same way.
Closure vs. Function Variable Access
A quick examination of variable access of a function versus closure reveals the following:
>> do func [x] [dt [loop 100'000'000 [x: x]]] 1 == 0:00:17.563 >> do closure [x] [dt [loop 100'000'000 [x: x]]] 1 == 0:00:13.641 >> dt in context [x: 1] [loop 100'000'000 [x: x]] == 0:00:13.563
A closure! will access variables faster than a normal function!, because on invocation of the closure function, its variables get directly bound to a context, just like an object.
A function! puts variables are on the data stack, so they are not directly bound. There is computational overhead to reach them, and that's what you are seeing above.
But note, it is not good to use closures all the time! A closure pays a very heavy price on invocation: It must bind its *entire* body and all sub-blocks before it evaluates. Depending on number of arguments, locals, and size of body (depth and breath) this takes some time (but, it is linear, not log, like in R2.) A closure also generates a lot more garbage than a function (for which it is possible, in R3, to create no garbage).
So, the cost of closure! is hidden, and can be much higher than that of function!. Let's turn the access tests into creation of functions and closures:
>> f: func [a] [a: a] >> c: closure [a] [a: a] >> dt [loop 1'000'000 [f 10]] == 0:00:00.468 >> dt [loop 1'000'000 [c 10]] == 0:00:01.343 >> dt [loop 10'000'000 [f 10]] == 0:00:04.437 >> dt [loop 10'000'000 [c 10]] == 0:00:29.39
Notice the non-linear time for C. This is caused by the 10 million new contexts created, which means a lot of garbage collection, when they need to be removed.