Comments on: MAKE object! - to copy or not to copy
I just noticed a comment in CureCode #802 from Maxim that deserves serious discussion.
Well, surprise! In R3 Alpha we've been deep copying object sub-series everywhere. This was got implemented early on - an experiment that wasn't openly discussed.
Frankly, I was waiting for someone to make some noise, sound the alarm. (It is still alpha.)
Maxim points out:
"This decision is so fundamental that it deserves to be addressed differently. There is need for deep copying objects, I can see some places where this would be useful, but there are also times when this is very evil."
"Data sharing is an integral part of a well designed system. What you propose feels like dumbing down REBOL."
Actually, it has not been proposed. It just "happened" long ago, and I'm not attached to it.
If we want to change this, we must do it very soon. The change is minor in REBOL, but has major impact on programs. (E.g. will break the current GUI demo, but we can fix that.)
With the binary any-string change also this week, I can see we're going to be very busy. Better plan to take your laptop with you on vacation to the south of France next week.
Come to think of it... that's the best place to make such a decision. Maybe we should all meet there for a mini-devcon? N'est-ce pas?
18 Comments Comments:
Ladislav 23-Jun-2009 7:56:40 |
Not copying objects found in the prototype was understood and promoted as a useful feature be me, so I am with Max in this. | Maxim Olivier-Adlhoch 23-Jun-2009 14:04:11 |
I'm happy this was raised, I was hoping it would become a blog topic. :-)
Since I've posted that I've been thinking about it almost daily and it occured to me that there is probably an easy way to solve the issue!
enable 'COPY on objects, and let 'MAKE go back to its v1 roots (all is shared).
I have circumstances where copying series already prevents some things from being possible, you cannot un-copy and you cannot trace the source of a string (was it part of original object, or specified in spec block).
copy could properly handle the /deep operation for objects too.
a combination of make and copy allows us full control on how objects are created.
ex:
a: make myobject [copy/deep subobject]
here only one specific part of a is deep copied, all the rest is shared.
using copy without the /deep refinement would effectively copy only the root elements, but retain their values, effectively making a new instance of an object, which shares previous data.
there are binding intricasies involved, but that, if properly documented is something you can control through the make/copy example above. | Maxim Olivier-Adlhoch 23-Jun-2009 14:16:08 |
oops typo in example above, should be:
a: make myobject [subobject: copy/deep subobject]
| Maxim Olivier-Adlhoch 23-Jun-2009 14:22:46 |
an alternative approach:
a: copy/deep/only myobject [suboject]
/ONLY could also apply to block copies. I've had situations where the above would have been so useful on blocks. | Peter Wood 23-Jun-2009 21:58:01 |
It looks as though Max would like something like JavaScript's object prototype behaviour in R3. Except he hasn't specifically asked for changes in an object's prototype to be reflected in the obejct yet.
Personally, I think the JavaScript model has a lot going for it. For instance, having a separately addressable prototype for an object does make things clearer than remembering if you used copy or copy/deep when creating the object. | Brian Hawley 23-Jun-2009 22:20:51 |
I wanted to get an idea about what R3 does currently, but ran into some trouble because SAME? doesn't work on map!, so I had to use ATTEMPT in my tests.
Here's the definitions:
a: context [
a: context []
b: []
c: make map! []
d: make vector! 0
e: ""
f: #{}
]
b: make a []
c: copy a
d: copy/deep a
Here is the test:
foreach [op x] [make b copy c copy/deep d] [
foreach [type y] [
object! a block! b map! c
vector! d string! e binary! f
] [
print [op type attempt [
same? get in a y get in get x y
]]
]
]
And here are the results:
make object! false
make block! false
make map! none ; same? fails, actually true
make vector! true
make string! false
make binary! false
copy object! false
copy block! false
copy map! none ; same? fails, actually true
copy vector! true
copy string! false
copy binary! false
copy/deep object! false
copy/deep block! false
copy/deep map! none ; same? fails, actually true
copy/deep vector! true
copy/deep string! false
copy/deep binary! false
Here are my preferred results:
make object! true
make block! false
make map! false ; maybe
make vector! true
make string! true ; maybe
make binary! true ; maybe
copy object! true
copy block! true
copy map! true
copy vector! true
copy string! true
copy binary! true
copy/deep object! false
copy/deep block! false
copy/deep map! false
copy/deep vector! false
copy/deep string! false
copy/deep binary! false
The maybes are because there might be merit in having the strings and binaries copy on MAKE to reduce potential problems, and having map! copy on MAKE is in my opinion more useful. Having blocks bind/copy on MAKE is a good idea, maybe even copy/deep, I don't know. This would be easier to decide if we had copy-on-write, at least of protected series.
How about your preferred results? | Brian Hawley 23-Jun-2009 22:34:52 |
Whatever we choose to have MAKE proto-object do, we're going to have to have initialization code in the spec block. The trick is to make the behavior that would require the cleanest and simplest initialization code for decent object models.
We have to consider multitasking, data objects, shared objects, differences we want map! values to have, data protected from modification, the extent we want to support stupid programming, whatever.
Balance it out and make a choice :) | Maxim Olivier-Adlhoch 24-Jun-2009 0:12:14 |
after the initial brain cramp of tracing the test ;-)
I agree completely with most of your preferred results.
the make results are task dependent, which leads to all manner of copy/deep/bind decisions (many variations in total for each type), and why you note most as maybe, cause in reality, there is no universal best case.
Hence, we should be allowed control, with a fast native by default for the most widely useful case.
it would be very easy to use a reserved word... which receives two arguments, the old proto and the new one with all words shared by default.
then you simply copy, copy/deep, reset, bind, whatever... based on what your system needs.
R2 implementation of make object could be simulated with something like:
context [
make-proto: [old new][
foreach word word-of old [if series? get word [set word in new copy/deep get word]]
]
]
make-proto wouldn't even have to be bound, all you'd be allowed to use is the new and old words.
it doesn't have to be friendly, just fast & mem efficient, since this is implicitely for advanced uses.
| Brian Hawley 24-Jun-2009 1:54:49 |
Darn, and after I went through all that trouble of putting those newlines in the code so it wouldn't expand the pre box :)
Every MAKE object! takes a spec block that is an init function, in effect. Every word in the prototype is already defined in the spec block at the start of its execution.
So the initial values of the words are your old, Maxim. And the values you set are your new. And the make-proto function is the spec block of: make proto spec
Your fast native with the sensible default sounds like MAKE. And your special-purpose function sounds a factory function written in REBOL, easy to make with either shallow COPY or MAKE proto. No keywords needed except the usual 'self :) | Brian Hawley 24-Jun-2009 20:23:57 |
I am now leaning against map! fields of objects copying on MAKE. Why not:
- You can't undo a COPY.
- Whether COPY or COPY/deep behavior is chosen, someone will want the other.
- It is easy to COPY or COPY/deep the map! in the MAKE spec if need be.
- The difference in behavior between map! and object! would be confusing, especially given how much effort we are putting into making them act similarly throughout REBOL.
I am leaning towards applying BIND/copy to any-block! and any-word! fields in objects, and leaving the rest as references to the original values. This will lower memory usage in the default case, and should be safe since we can PROTECT the other types (in theory). If we need to copy, we can do so in the spec block. | Brian Hawley 24-Jun-2009 21:00:15 |
Addendum to the last message: Functions and closures assigned to object fields should BIND/copy on MAKE too :) | Maxim Olivier-Adlhoch 25-Jun-2009 3:02:27 |
Brian, the point of having a make-proto within the function is not to replace the init.
its to make it the default of an object you are going to make over and over. how an object copies is as important as what it does.
basically you are creating a persistent spec block.
but if the make already copies some data, then the init func OR spec can't help you.
if the init where implemented as I show, then the burden of the "proper" method is not anyone's to prove. A default is chosen, which anyone can change.
I don't want blocks to be bound... blocks are data. binding has to be explicit, just like reducing. the old aggressive evaluation of REBOL was similar in that it didn't allow you the choice to reduce or not in some circumstances, IIRC, and it was very annoying. | Brian Hawley 25-Jun-2009 3:36:27 |
Maxim: "I don't want blocks to be bound... blocks are data."
Say goodbye to local variables in PARSE rules then. Code is bound, but that doesn't make it data any less.
The point of MAKE is to be the sensible default. Look at the proposed results above: I am not just suggesting behavior for MAKE, but also for COPY and COPY/deep.
If you don't want blocks to be bound then COPY is what you want, not MAKE. Write your factory function starting with COPY - a shallow copy that just references the original fields - and then do any deep copying or binding you like to the data or not. No keywords needed.
Maxim: "basically you are creating a persistent spec block."
If you want to create a persistent spec block, why not just create a persistent spec block? It's much easier than what you suggest. | Oldes 25-Jun-2009 15:24:38 |
I like MAKE which does not copy and COPY which copies, so basically I agree with Brian's preferred results above. | Maxim Olivier-Adlhoch 28-Jun-2009 14:45:52 |
in fact, as long as:
-
COPY only copies the "outer shell" of the object and shares everything inside
-
MAKE doesn't deep bind objects.
I'm happy.
I can build a NEW function using COPY. something I couldn't do before with R2. | Giuseppe Chillemi 1-Jul-2009 15:15:53 |
Hello,
I am not a professional REBOL programmer but I faced this problem a couple of years ago. My wish was to have values or functions shared with the previous object used to generate the new one.
At that time I got some answers from Gabriele which told me that I should create a sub-object because sub-objects are shared among different objects. I didn't like the situation but I accepted it.
Now I read about this topic and I can give my opinion: I like a lot data sharing among objects and I prefeer this solution rather the "copy everything" approach used in REBOL2. I wish to choose what to share and what to copy.
Giuseppe Chillemi
| meijeru 4-Aug-2009 3:33:31 |
I suspect functions are also copied, not shared. I am implementing a system in which there are many instances of objects with variable data and access functions working on them. To have a separate copy of each access function in each instance of the object is wasteful. Therefore I am all for sharing (not copying) when applying MAKE. The user can copy(/deep) if they want. | Brian Hawley 4-Aug-2009 14:31:34 |
Meijeru, you are missing that functions are not copied, they are BIND/copy'd. If you have access functions that refer to fields in an object that are not passed as parameters, the words that refer to those fields need to be bound to the object. REBOL has direct binding, not nested scopes.
This is why REBOL applications that use accessor functions tend to have those be functions that operate on an object, rather than functions of an object. REBOL OOP doesn't use methods - we don't have classes unless you implement them manually. Which is easy: A class is just an object containing a set of functions that operate on other objects with a consistent set of fields, one of which could be a reference to the class object.
Overall, REBOL tends to be better at procedural, modular and functional code than at class-based OOP code, but there are some exceptions, such as the GUI. As long as you follow the procedural-OOP or functional-OOP models instead of the method-OOP model, your code should be efficient. |
Post a Comment:
You can post a comment here. Keep it on-topic.
|