The behavior of the following in historical Rebol/Red strikes me as extremely buggy:
rule-ctx: [num: 1020]
rule: [num 'foo]
bind rule rule-ctx
parse-obj: make object! compose [
rule: (rule) ; assume this is the non-splicing semantic
run: method [data] [
parse data rule
]
...
num: 304 ; imagine this is just an incidental field name
]
-
The COMPOSE'd rule will be bound to PARSE-OBJ's
num
instead of RULE-CTX'snum
. The more generic the word, the more likely this is going to happen on accident. You get different behavior than if you'd named the rule something else, and not used COMPOSE... as with make object! [rule: a-rule, ...], because then the block wouldn't be fetched via word when the body runs. It would be the worda-rule
that was hit with the binding wave, not what it looked up to. -
Not only was the COMPOSE'd rule changed, but the original rule is changed! This is because there's nowhere to store unique binding information for the rule as seen through the lens of the object vs. not. It is mutably bound, and the only way to avoid this is to make a copy.
As it happens, I actually have some progress to report on #2 with virtual binding. I'm making it possible for multiple "views" of the same block to see it bound different ways (a technique which started with having multiple views of a function's body viewed via different frame instances, that is extended to stacking on views through arbitrary objects).
But that doesn't do anything to help #1.
MAKE OBJECT! Is Actually Three Operations
There are three distinct steps performed historically by MAKE OBJECT!.
-
COLLECT all the top-level SET-WORD!s in the BLOCK! to make an empty OBJECT!, with all those words unset.
-
BIND the block's ANY-WORD! elements to the newly created object.
-
DO the bound block.
Virtual binding means the second step can be very fast. The block is just annotated to say "you are a view as seen by this object". (It's possible to deep-walk the block and do some a-priori caching to make the execution the first time faster, but that's not necessary.)
Without virtual binding, the second step requires a deep walk... and destructively binds the block so any other references to the block are now contaminated. This is usually not what you'd want, but sometimes (as when modules are being loaded) it's intended.
What If MAKE OBJECT! Was Just The COLLECT Step?
If we let you make an object from a block without actually binding or running it, you could tailor the steps as appropriate for your operation.
For instance: Modules don't want virtual binding...they fabricated a new block themselves from the input UTF-8. They want to destructively bind their single copy to lib and the module:
block: transcode read module-filename ; oversimplified...
mod: make module! block ; imagine this does *not* run the block
bind block lib
bind block mod
do block
You get another advantage: you have access to both the result of the DO and the result of the MAKE. These could be provided as separate outputs from something like IMPORT.
If you wanted something that was a cleaned-up version of today's MAKE OBJECT! that used virtual binding to avoid contaminating the input block (e.g. because it was read-only material in a function body, or passed as a const parameter), you could say:
obj: make object! block
do in obj block ; I've repurposed IN to be the virtual binding operator
obj ; the object as the result
This still has the possibly unintended effects of #1 above, where all COMPOSE'd components inherit the binding. But you could use a more conservative virtual bind that only applied to the top level set-words. I have that implemented, it would just need to be exposed somehow:
obj: make object! block
do in/shallow/set obj block
obj
This would make binding into the object optional. If MY were like METHOD and took the binding on the left and propagated it to the right, you could say either:
make-obj-toplevel compose [rule: (rule), ...] ; no binding effect
make-obj-toplevel compose [rule: my (rule), ...] ; block bound into object
And if MY were powered by virtual binding, that wouldn't have to touch the original rule.
For that matter, with only top-level words assigned, you'd get the advantage of not even needing a COMPOSE. Any normal words would be left as they were:
make-obj-toplevel compose [rule: rule, ...] ; different `rule`s
This Seems A Promising Direction
It needs to be hammered out in terms of the details, and names. We have names like CONSTRUCT and CLASS that we are looking at. I don't particularly like CONTEXT or OBJECT as verbs since they are type names.
There's an uneasy question about whether MAKE should be a high-level operation (due to its short name, wanting the high-level operations to be in reach). However, while its name is short, it has to be combined with a type which makes it not all that short compared to e.g. CONSTRUCT.
I experimented for a time with HAS as seeming analogue to DOES. It didn't catch on at the time, I suspected because it sounds more like a question than a statement. But it is does have brevity on its side:
obj: has [
x: 10
y: 20
]
I'll throw it back out there. Anyway, my natural leaning would be to say that MAKE becomes the low level operation that doesn't actually run the block you give it, then these other higher-level variations get names.
This would also seem consistent with MAKE FRAME!, which gives you an empty (seeming) context. But we could also distinguish make*
from MAKE, or variations of that.
Afterthought: I'll also remind people about WRAP, which was an operator I proposed long ago that was like MAKE OBJECT! but returned the result of the evaluation...which could be more convenient now:
>> wrap [x: 1000, y: 20, x + y]
== 1020
This gives you the same result as use [x y] [x: 1000, y: 20, x + y]
by automatically collecting top-level SET-WORD!s. This could make a comeback...more efficiently expressed with virtual binding.