Mailing List Archive: 49091 messages
  • Home
  • Script library
  • AltME Archive
  • Mailing list
  • Articles Index
  • Site search
 

[REBOL] Re: Embedded Object and Scope - again...

From: joel:neely:fedex at: 10-May-2001 9:11

Hi, Christophe, Pardon the repetition if I cover anything you already understand, but I have a possible (possibly partial?) suggestion below. This post is longer than I'd like; it's the result of my exploration of object contexts. I didn't have sufficient editing time to make it more concise. My conclusions (for anyone who doesn't want to wade through the background reasoning) are: * RT chose the present implementation as the most economical way to get object/method behavior, given the unique way REBOL implements objects (as private namespaces). NOTE: THIS IS AN INFERENCE ON MY PART, NOT OFFICIAL DOCTRINE! * If we understand something of how RT handled functions in objects, we can implement a corresponding trick for your current problem of embedded methods. * There are a couple of ways to do so, one of which is, "Don't do that!" * Construct a new object from a context-stripped representation of the old object's state. new-obj: do mold old-obj * Construct a new object from the same spec used to construct the old object. old-obj-spec: [ ...blah blah blah...] old-obj: make object! old-obj-spec ... new-obj: make object! old-obj-spec The choice between these depends on whether you want any changes over the lifetime of OLD-OBJ to be reflected when NEW-OBJ is constructed, or whether you want NEW-OBJ to be constructed with the same *original* state as OLD-OBJ. -jn- CRS - Psy Sel/SPO, COUSSEMENT Christophe, CPN wrote:
> ... an example which I've simplified to the following ... > >> ob1: make object! [
[ a: 1 + 2 + 3 + 4 [ e: make object! [ [ b: 100 [ f: func [] [a + b] [ ]] It's instructive (to me, as I need instruction beyond what the available docs cover ;-) to peek under the hood at this point:
>> source ob1
ob1: make object! [ a: 10 e: make object! [ b: 100 f: func [][a + b] ] ] The first observation is that OB1/A is shown as being set to a literal 10, not the original expression that was evaluated to produce that 10. The spec block is evaluated during object construction from that block.
>> glorp: make object! [
[ a: 1 + 2 + 3 + 4 [ b: first "Yowp!" [ ]
>> source glorp
glorp: make object! [ a: 10 b: #"Y" ] So the "source" for an object is not "the source" from which it was created, but simply "a source" which represents the current state of the object. The second observation is that using an object as the spec for construction of another object involves using the current state of the first object as a "guide" for constructing the second. Many value types are deep cloned in the new object, as in:
>> blob: make object! [
[ a: 1 + 2 + 3 + 4 [ b: [[["test"] 1] 2] [ ]
>> blab: make blob [] >> change blob/b/1/1/1 #"T"
== "est"
>> source blab
blab: make object! [ a: 10 b: [[["test"] 1] 2] ] However, function values are handled differently; if functions in an object are to behave as conventional OO methods, words in the old function that are defined in the old object's context must be replaced by words in the new function that are defined in the new object's context. (Whew! What an awful sentence!) For example:
>> w: "Global"
== "Global"
>> bb: [w]
== [w]
>> ff: func [b [block!] w] [
[ append b [w] [ ]
>> ff bb "arg"
== [w w]
>> ob1: make object! [
[ fff: func [b [block!]] [ [ append b [w] [ ] [ w: "in ob1" [ ]
>> ob2: make ob1 [w: "in ob2"] >> ob1/fff bb
== [w w w]
>> ob2/fff bb
== [w w w w]
>> reduce bb
== ["Global" "arg" "in ob1" "in ob2"] So that BB now contains (references to) four words, all of which happen to be named W, and all of which are defined in different contexts. This shows that a (reference to a) word drags the word's context along with it in order to make it possible to find that word's value. (That's also a sloppy sentence, but in absence of hard documentation, it's the best I can do at the moment. I'll be happy to receive instruction from anyone who can replace it with a more precise one.) The point for this thread is that the last two occurrences of W are supplied by the method FFF in each of the two objects, which have been made to be relative to the object in which they occur. This business of constructing a new function from an old function that serves as a prototype/template is NOT just a matter of deep cloning, as the cloned words would still have their original context. Instead it must involve the construction of a new function with a new context. The relationship between words in the old and new functions is: Old function word/context New function word/context --------------------------- --------------------------- - local to old function - local to new function - in old enclosing object's - in new enclosing object's context context - otherwise - same as old (And, as always, I'll be grateful for any further light anyone can shed on this.) You can see that this is not a trivial change (nor a cheap one) but is necessary if we are to get the equivalent of standard OO methods in object-contained functions. However, it is expensive enough that we don't want to do it except where absolutely necessary. The case of an embedded object is not as strongly compelling as that of an embedded function, so references to embedded objects are just cloned (because it's cheaper to do so, is my conclusion). Thus, with the original (simplified) example, we get the expected behavior:
>> ob1/e/f
== 110
>> ob1/a: 20
== 20
>> ob1/e/f
== 120 But using OB1 as the spec to create OB2 caused some surprise:
>> ob2: make ob1 [] >> ob2/a: 30
== 30
>> ob2/e/f
== 120 I'll leave it as an exercise for the reader to see how this:
>> print get first second third second third second ob2
20 proves that OB2/E/F still refers to OB2/A ;-).
> Ouch, not what I thought... Let's check... In the "Official > Guide", pg 348, I found: > > The ancestor object and the descendant object share the > same embedded object. >
Which we just demonstrated above.
> OK, so I tried something else: > > ... example simplified below .... > > Again, not what I thought ... I was awaiting 20 !!! ;-( > >> ob3: make ob1 [
[ d: make e [] [ ]
>> ob3/a: 40
== 40
>> ob3/e/f
== 120 We didn't get 140 (this example's equivalent of your 20), because of the way OB3/D is constructed. For reference...
>> source ob1
ob1: make object! [ a: 20 e: make object! [ b: 100 f: func [][a + b] ] ] OB3/D is built using OB1/E as a spec. Therefore, it must have its own B (which gets cloned) and its own F... AHA! Since OB3/D/F is a function (see the discussion of making new object-method functions above), we have to make sure that words in OB3/D/F are bound to the correct contexts. Therefore, the B in OB3/D/f *must* refer to the B in OB3/D -- the immediately-enclosing object for OB3/D/F. However, the A in OB3/D/F isn't local to OB3/D/F, and isn't an object field of OB3/D. REBOL doesn't look any further for containment, so A is just treated as an ordinary (i.e., non-local, non-enclosing-object-field) word and the new A has the same context as the old A (in OB1/E/F).
> Here are my questions: > > 1. Why did RT choose for such implementation? > What are the advantages ? >
As mentioned above, we *MUST* have context-twiddling for word in functions that refer to fields of the immediately- enclosing object, or else we couldn't have object methods. As to why the same logic isn't propagated upward through multiple layers of enclosing objects, I can only speculate that RT: 1) simply didn't think of it 2) thought of it but didn't want to incur the expense, assuming that it wasn't as compelling a situation, or 3) implemented the context mechanism in a way that made it infeasible to chase up a list of "nested contexts".
> 2. Why did my last try not work - it was -IMHO- logical > I should get a result of 20 ..., as I instanciated the > embedded object within the instanciation of the ancestor > object. >
Because multiple levels of embedding are not traversed. I hope my analysis of the construction of OB3 answered this question. If not (and if you want more of the same tedious pedantry ;-), let me know.
> 2. How can I get my descendant-embedded-object to retrieve > the property of it's container, the descendant-object ? >
I can think of a couple of ways to get this effect. 1) Construct a new object in a way that strips off context for all words. Remember that the "source" of an object is a representation of how to build an object in the current state of the one we're looking at. Therefore, we could say:
>> ob4: do mold ob1 >> ob1/a: 99
== 99
>> ob1/e/f
== 199
>> ob4/e/f
== 120 When we MOLD OB1, we get a string that represents source for the current state of OB1. When we DO that string, we get an object that is similarly structured to OB1, but *ALL* words throughout that object and its nested content are now doing what we might have expected. 2) Save the original specification, and use it to construct new objects that are to be "of the same class" as the old object.
>> ob-spec: [
[ a: 0 [ e: make object! [ [ b: 100 [ f: func [][a + b] [ ] [ ] == [ a: 0 e: make object! [ b: 100 f: func [] [a + b] ] ]
>> ob5: make object! ob-spec >> ob5/a: 43
== 43
>> ob5/e/f
== 143
>> ob6: make object! ob-spec >> ob6/e/f
== 100 Of course, the difference is that changes over the lifetime of OB5 will affect only OB5 and not OB-SPEC. Therefore OB6 starts life as an image of OB5's *original* state, not its *current* state. If you really need the current state, you can use the DO MOLD trick instead. Thanks for the stimulating questions! -jn-