Ranting about some object theory...
Every since the whole discussion about object variables and object references in Studio came up this has been linguring in my mind so before I continue with my discussion on a way to use object variables in Omnis I felt compelled to do some ranting :)
I can't be certain but I imagine this discussion lies at the heart of the decision making of how Omnis originally implemented their object model and what direction they took it in.
Take the following very simple bit of pre OO code as an example:
Calculate lvNumberA as 4
Calculate lvNumberB as lvNumberA
Calculate lvNumberB as lvNumberB+1
Every programmer instinctively knows that the end result of this code results in lvNumberA being equal to 4 and lvNumberB being equal to 5. The two variables are two distinct entities; assigning one to the other means that their contents temporarily becomes the same but that is all they share and only momentairily.
Now lets take a theoretic leap into the OO world and say that an integer value isn't a separate entity but is an object having that value as its state.
So we have a class called integer; this class contains a single value as its state. lvNumberA and lvNumberB are now both instances of this class. The "calculate A as B" syntax (for the non Omnis people reading this, it's Omnis' syntax for A=B) simply translates to an $assign call on the object. For clearities sake I'll not go into the discussion of the OO concequences of lvNumberB+1 if it was taken literally; let's just have faith that somehow this gets handled without influencing the contents of lvNumberB directly and that somehow we get the integer value contained within lvNumberB.
The above code would be translated behind the scene's to this:
Do lvNumberA.$assign(4)
Do lvNumberB.$assign(lvNumberA)
Do lvNumberB.$assign(lvNumberB+1)
In this notation it becomes absolutely clear that yet again lvNumberA and lvNumberB are two separate instances that at one point in time simply share eachothers state as the state of A is copied to B.
As this is the behaviour anyone working with Omnis 7 was expecting and was formiliar with it made perfect sence to apply the same to any other object one would create. Calculating B as A simply means that whatever state A was in was copied to B and from that point onwards they go their marry way. It works for simple integers, it works for complex objects with vast amounts of data (albeit taking a bit more time to execut). The first time A or B is used the required memory is allocated accompanied with a construct call if applicable; whenever a new state was assigned the old state was overwritten by the new; and whenever the variable goes out of scope the memory was simply deallocated.
A destruct was omitted; if in our example lvNumberA and lvNumberB would indeed have been objects (the code would look slightly different but that again is besides the point) lvNumberA would have gotten a construct and then get the assign call; lvNumberB would never have been constructed but simply has memory allocated to receive a copy of the lvNumberA's state; and finally at the end of the method when both variables go out of scope lvNumberA and lvNumberB are deallocated. If either lvNumberA or lvNumberB would have been returned to the calling method we could potentially be faced with calling two destruct methods for an object that was constructed once and for which another copy still lives on in the calling method. This was probably deamed to confusing and as such doesn't happen.
Before I continue with this line of thought; the way in which Omnis returns a value is probably the most critical edge Omnis has in this discussion.
In C++/Obj-C and I imagine many others we would be faced with a dilema (and now with Object References, we in Omnis too have the same dilema I might add). Do we return our object (pointer) with the risk that whomever called our method is unaware of the need to destruct that object if he/she decides not to use it (with a high likelyhood of memory leaks being introduced, he/she may not even be aware of the object being returned in the first place or it may have been an addition to the code later on) or do we nicely destruct our object in which case we can not return it because it no longer exists.
Obj-C solved it by introducing a release pool (allowing you to release the object at some later cleanup stage); Java/C# came up with garbage collection in which case the object does not get destructed but some magical process (phun intended) at some much later date and time suddenly realizes there are megabytes of unreleased objects that are unused and should be cleaned up.
Omnis however has the high ground. In the way variables are returned to calling methods it has all the knowladge in hand to see if the object is retained by the calling method or whether the object will become unused when the method ends. In a way Omnis is doing the following:
Calculate lvMyObject as $objects.anObject.$new() ;; create a new instance of an object
; Do some stuff with lvMyObject here
Do returnObj.$assign(lvMyObject)
; Now destruct lvMyObject as it goes out of scope
returnObj doesn't actually exist but it represent whatever variable the calling method uses as a return value. If there is no return variable the step is simply ignored. Obviously with object variables whatever variable returnObj would be it would get a copy of the state of lvMyObject and as such becomes a new instance with a life of its own. In this case lvMyObject being a local variable there is no issue but obviously we could just as well have been returning an instance variable that also lives on as a separate copy.
Alright, let's go back a bit. We get state copied on each assignment and as such getting a destruct on each copy when that copy goes out of scope makes no sense. Still I've already admitted that the principle seems very sound. Let's look at C++ for a second. You may not realize this but C++ acts remarkably the same with object as Omnis does with Object variables and Object References.
Look at this bit of C++ code (my C++ is a bit rusty so pardon my typos):
MyObjectClass::MyObjectClass() {
m_value = 0; // init as 0
};
MyObjectClass::~MyObjectClass() {
// nothing to do here...
};
void MyObjectClass::assign(int p_value) {
m_value = p_value;
};
int MyObjectClass::value(void) {
return m_value;
};
// ...
MyObjectClass ObjectA, ObjectB;
ObjectA.assign(4);
ObjectB = ObjectA;
ObjectB.assign(ObjectB.value() + 1);
Notice that I am not using pointers. The last 4 lines do the following:
1) both ObjectA and ObjectB get constructed and their internal value set to 0
2) ObjectA get's 4 assigned to it
3) ObjectB memory get's overwritten with ObjectA
4) ObjectB returns its value, 1 is added and the result is send back to ObjectB (OO police arrest me :D !)
5) both ObjectA and ObjectB get destroyed
There are three differences with Omnis at this point, both objects get fully constructed (while in Omnis only object A gets fully constructed while object B would only be allocated), C++ allows for overriding the copy and lets you add additional logic (not using that here), and both objects get fully destructed (while in Omnis they only get deallocated).
In C++ however you don't see objects being used like this very often, more common is the use of pointers which are nearly identical to Object References in Omnis. Omnis only has a slight high ground in tracking the instances even after all the object references are out of scope and that you don't get the blue screen of death when calling a method on an object reference that is pointing into nothingness but thats just about it (two key improvements though; both of which are also in Cocoa in another form:D ).
Anyways, now with pointers:
MyObjectClass *ObjectA = new MyObjectClass();
// ...
ObjectA->assign(4);
ObjectB = ObjectA;
ObjectB->assign(ObjectB->value() + 1);
// ...
delete ObjectA;
// here lies the most dangerous moment in my code...
There are a couple of fundimental changes here. First off, I only instantiate one object. The line where I copy ObjectA to ObjectB actually doesn't copy the object but copies the pointer, ObjectB becomes ObjectA. There is only one instance. As a result, after I add 1 to the value of ObjectB, ObjectA also reflects this change as ObjectA is the same thing as ObjectB. Suddenly a piece of code that looks identical to the situation before acts completely different (naming conventions help here, but thats a different story).
Further more when I destruct ObjectA I have destructed the only copy I created and thus ObjectB's value is no longer valid either. There is no more object. If I am not aware of ObjectA and ObjectB being the same object I may get into serious trouble. Obviously in this piece of code it is very apparent but just imagine these lines being spread out over hundreds of lines of code.
It suddenly becomes very important to be aware of the number of pointers out there pointing to the same instance of an object and whether it is to early to destruct your object because it is still being used.
There is no difference in using pointers in C++ as it is in using Object References. The big question however is why would you be copying pointers to the same object if it is so confusing in the first place?
I'm sure that is the question Omnis asked themselves way back with Studio 1 and without finding an answer to that question decided to spare its developers this agony and hence stuck to the very understandable, safe and predictable way object variables work. To some extent they where right. Though Object References allow you to do things impossible with Object Variables, you don't really need them and their full complexity until you start doing some really funky stuff. Legions of Omnis Developers find that they can do their job just brilliantly and without all the headaches sticking with Object Variables just fine.
Yet at the same time there is something really liberating about being able to point to the same instance of an object from multiple places in your application. I don't want to go into that to much here as it strays from the subject, I could write pages and pages about this and will touch on this more and more as I talk more about Object References in other posts.
What gets me is how close Object Variables are to creating the best of both worlds. The model could be enhanced to give us both options. Whether Omnis should start with Object References and add some flavour to it or go back to Object Variables and enhance that. It really depends on some of the internals that are hidden from me. I know some of it through my work on XCOMPS which leads me to believe TL is very close.
So here is my suggestion, I'm sure it will fall on deaf ears but who knows someone might find it useful. I'm going to start from Object Variables and suggest changes to that to illustrate the end result I would suggest.
1) When encountering a "calculate objectB as objectA", release objectB if it is already instantiated (this should actually be done after the copy in case we are using the original ObjectB in the right side of the equation but thats a piece of cake to implement) and allocate new memory for objectB if it did not yet exist. Then copy the state of objectA into the new objectB excluding any memory management data and finally call $copyconstruct on objectB with objectA as a field reference parameter (this would be a nice bonus giving us some more control in case we also want to change/reset some of the state). On a "calculate objectB as #NULL" obviously only the release on objectB will be performed.
2) When encountering a "reference objectB to objectA" (a new command) objectB simply gets the same pointer as objectA and a retain is called on the instance. If objectB was already instantiated release is called on that instance (again after the retain on objectA just in case; order is important here, retain the new object first, then release the old).
3) when encountering a "quit method objectA" and there is a return variable in the calling command copy the pointer of objectA and call retain else ignore the command; if there is a worry about being backwards compatible with logic "quit method object" could result in a copy while a new command "quit method with reference objectA" allows for returning the actual reference (or a new switch on the quit method command that allows the developer to choose).
4) whenever a variable goes out of scope call release on its instance (if it contains one).
5) if a retain results in the reference count hitting 0 (the last variable referencing it goes out of scope) $destruct is called on the object and it is deallocated
The end result for our original (slightly modified so it would be possible in Omnis actually using an object class) example of having two local object variables would be:
- "Do lvNumberA.$setValue(4)" gets executed resulting in lvNumberA getting constructed, it's $construct getting called and its $setValue getting called which sets an internal instance variable to 4
- "Calculate lvNumberB as lvNumberA" gets executed, as lvNumberB does not yet exist memory gets allocated, the contents of lvNumberA gets copied to lvNumberB and $copyconstruct gets called with lvNumberA as a parameter. Assuming $copyconstruct isn't overriden lvNumberB gets the same state as lvNumberA but with a new reference count of 1.
- "Do lvNumberB.$setValue(lvNumberB.$getValue()+1)" gets executed setting the internal instance variabel of lvNumberB to 5
- "Quit method lvNumberB" results in lvNumberB getting copied into whatever return variable
- lvNumberA and lvNumberB both get $destruct called and get deallocated.
- Our return variable lives its own life until it also goes out of scope and its instance gets destructed.
Now using the reference commands:
- "Do lvNumberA.$setValue(4)" does exactly the same
- "Reference lvNumberB to lvNumberA" makes lvNumberB point to the same object as lvNumberA and the reference count becomes 2
- "Do lvNumberB.$setValue(lvNumberB.$getValue()+1)" again set our value to 5, but now on our one and only instance
- "Quit method with reference lvNumberB" assigns the same object to our return value and increases the reference count to 3
- lvNumberA goes out of scope and releases the object bringing the reference count back to 2
- lvNumberB goes out of scope and releases the object bringing the reference count back to 1
Now whenever our return value goes out of scope assuming it doesn't assign even more references the reference count goes to 0 and the object destructs.
Add to: Digg | Technorati | del.icio.us | Stumbleupon | reddit | Furl
just became aware of your blog through Google, and found that it is truly informative. I am gonna watch out for brussels. Iâ