Breaking MAKE OBJECT! Into Component Operations

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
]
  1. The COMPOSE'd rule will be bound to PARSE-OBJ's num instead of RULE-CTX's num. 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 word a-rule that was hit with the binding wave, not what it looked up to.

  2. 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.

2 Likes

This indeed seems a good direction. Certainly leans against using MAKE as the means of creating derivatives:

account: new-object-constructor-that-works-the-old-way [
    name: "Flintstone"
    balance: $100
    ss-number: #1234-XX-4321
    deposit:  func [amount] [balance: balance + amount]
    withdraw: func [amount] [balance: balance - amount]
]

other-account: make account [
    name: "Rubble"
    balance: $200
    ss-num: #012-XX-3456
]

I presume a streamlined MAKE would now enforce the following form?

do in other-account: make object! account [
    name: "Rubble"
    balance: $200
    ss-num: #012-XX-3456
]

; sort of a parallel to the way the following
; similarly makes a clone
make text! "Foo"
1 Like

Over on the CONST thread, I mentioned a property of generic quoting that I found disconcerting, in that it subverted CONSTness. Because there was no other clean way for the API to bypass an "evaluative wave of constness" otherwise.

I resolved to put my discomfort aside, and accept that generic quoting offers a way for things to be put into an evaluative situation...behaving as-if they had been accessed by a WORD!. Because sometimes (e.g. in the API or when COMPOSING) you don't have a word to access through...to get that behavior if it's what you wanted.

The proposed workaround for when that const subversion was not intended was that people use just (...) (e.g. give me that group literally, I'm still liking JUST instead of LIT for that) instead of '(...) in cases that they wanted the constness to match the flow of the block.

Which reminded me of the situation in this thread...

The two connected in my head, to ask the hypothetical question what if quoted things had their binding left untouched...as if the thing you get had been fetched by a word, instead of subtracting a quote level?

some-rule: [num: 10]

make object! [rule: some-rule, num: 20]
;   word access, no binding influence on RULE

make object! compose [rule: (some-rule), num: 20]
;   becomes make object! [rule: [num: 10], num: 20]
;      following the (...) is don't splice and ((...)) is splice convention
;   today this would virtually bind NUM in the rule member into the object
;   so from RULE's point of view, NUM is 20
;   but thanks to virtual binding, SOME-RULE is unaffected

make object! compose [rule: '(some-rule), num: 20]
;   becomes  make object! [rule: '[num: 10], num: 20]
;      because COMPOSE keeps the decoration on the composed thing
;   what if the quote subverted the virtual bind, as in the WORD! case?
;   so the NUM is seen however it was in SOME-RULE?

A wild--but promising--thought.

Though it raises the question of how things in quotes would ever get bound--such as the SOME-RULE inside of '(some-rule). So if this technique were used, it couldn't apply to all bindings.

But where it did apply, it would mean using a literalizing operator that didn't employ quotes (like what is proposed as JUST) more than we do now. And there would be a lot of unbound things, since most quotes aren't programmatic with QUOTE or COMPOSE.

When this was in effect, you couldn't write stuff like get 'x (which you wouldn't usually, because you could say just x or :x)...but you'd have to say get/any just x instead of get/any 'x. If you were going to write if condition [[a block]] that would be different from if condition '[a block].

It would mean that append block [x] and append block just x would mean something different from append block 'x.

It has a plus side in data exchange. I've talked about the hazards of "stray bindings", not only do they offer possible unwanted linkages to internals of things that weren't intended when using words as a kind of enum symbol, they also hold GC things live which they may have no interest in.

PARSE's mechanics for recognizing things literally wouldn't be affected, as bindings aren't heeded by the matching process. parse [[a] [a]] [some '[a]]

Anyway... I just saw some potential synergy with the "act like you got it from a word" issue run up against in const. I've mentioned how the problems that you run into with the API are no different than the issues you run up against with COMPOSE when you don't have getting things out of a variable...which is why rebQ() is so important...but it also points out that this comes up in non-API scenarios, like this binding problem.