|
Prev: Why is Object Oriented so successfull
Next: Dreamworks Animation is Looking for a UI Engineer/Designer
From: Dirk van Deun on 14 Apr 2008 07:17 I have been thinking for a while now that there ought to be something in between subclasses on the one hand, and mixins on the other. I'll explain what I mean. Subclasses apply to one class only, but they have the nice property that they access the members of their superclasses through lexical scoping. Mixins on the other hand can be applied to several classes, but they do not have the members of their superclass in scope, so that references to members of superclasses only get a meaning at the moment when the mixin is composed with a class: mixin and class are then stitched together based on string comparison of identifiers. This difference becomes important when the class in question is itself composed of a class and a previously applied mixin: through its "flattened" view, without real scoping, a second mixin cannot know or specify whether an identifier will refer to something in the original class, or something in a first mixin, so that the order of mixins determines the meaning of the whole in an ad hoc way. My goal then would be a language with components that are more reusable than subclasses, but with real lexical scoping: or something in between subclasses and mixins. After having prototyped a language that has something in between, I thought it a smart idea to stop playing with toy examples for a while and seek the opinion of some Real Programmers: does my system actually solve any real problems, or just some favourite academic examples ? To solicit your opinion, and explain what the system is, I have to quote the first three sections of the description of a toy language that has no subclasses at all. This is only a bare minimum to get my point across, but I do not want to scare away *all* potential responders. :-) Let us define a simple class Human as follows: class Human { method play_tetris() { ... } } Now in languages with subclassing, to derive a subclass Woman, we would, using some specialized syntax like "extends", reopen the scope of Human, and embed the class Woman inside it, so that the methods and data of the class Human would be visible for any newly added methods for Woman. Abusing Tingle syntax, as the language does not allow of subclasses, we would write something like: class Human { class Woman { method buy_more_shoes() { ... } method give_birth() { ... } } } The two new methods are able to use the Human method play_tetris(), which is in an enclosing scope (for instance to while away the time until a shoe salesperson is available). In our language without subclassing, the above code is syntactically valid; but it actually means that the classes Human and Woman overlap, and that the two new methods exist in their intersection, available only to objects that are both Human and Woman. As all women are by definition also human, the class Woman and the intersection coincide. Maybe we can make the example more interesting by making it about the intersection of Human and Female instead: class Human { class Female { method buy_more_shoes() { ... } method give_birth() { ... } } } class Woman = Human Female We now have two independent classes, Human and Female, neither of which is a super- or a subclass of the other; and thus we see that we need a separate composite class Woman, for objects that are both Human and Female. As order is irrelevant for set intersection, we can change the order of nesting: class Female { class Human { method buy_more_shoes() { ... } method give_birth() { ... } } } Now we can express something that we could not express earlier: the fact that give_birth() is typically female, but buy_more_shoes() only applies to human females: class Female { method give_birth() { ... } class Human { method buy_more_shoes() { ... } } } As might be expected, a more specialized give_birth() for human females can be added in the intersection. If we would create an object of the class Female without composing it with Human, it would provide the basic method give_birth(), and no buy_more_shoes(). If we do create an object of the class Woman, on the other hand, all most specialized methods will be available. Methods in the intersection of Female and Human will predominate over methods with the same name in the composing classes. For a Woman named mary, mary.method() will refer to a method in the intersection if a method with the right name is available there; if not, it will refer to a method in either Human or Female. As there is no hierarchic relationship between Human and Female, when both the classes Human and Female define a method and the intersection does not resolve the conflict, neither version of the method is predominant, and it is an error to try to use it. For a composite class that combines more than two classes in which a same method name occurs several times, the predominant method, if it exists, is the unique method that occurs in the intersection where all the classes and all the intersections that define that name overlap. Or to put it differently: specialization is a partial order among methods; if there is one version that specializes all the others, that one is the predominant method. If not, there is none. Now let us reuse the class Female: class Bovine { method look_at_passing_trains() { ... } } class Cow = Female Bovine For a Cow, only the part of Female outside of the intersection with Human is relevant: the intersection with Human is simply ignored in composing this new class. The intersection of Bovine and Female on the other hand is still empty, so let us now add a method moo(): class Bovine { method look_at_passing_trains() { ... } class Female { method moo() { ... } } } The method moo() characterizes Bovine rather than Female -- I personally associate the concept of mooing with bovines rather than with femininity -- and that is why I add moo() to the definition of Bovine, further nested in Female, rather than the other way around. In section II we will see that the order of nesting, which is irrelevant for determining the predominant method, is however relevant for scoping, so in quite some cases the order of nesting will probably rather be chosen with scoping in mind. The class Female was reopened for this example: classes can be reopened as often as needed, as an outer nesting as well as nested in others. On the other hand, I did add the new code to the Bovine-block I wrote earlier, instead of reopening Bovine, but only because the two methods seem to go well together thematically. When you arrange methods thematically by reopening classes several times, tools can easily be provided, for instance as editor plugins, to view the code by complete classes, or to see all variables in a certain scope together, when this helps understanding. Tools for the opposite direction would be much harder to conceive. II Scoping The order of nesting of classes is irrelevant to the identity of predominant methods; but it remains the foundation of scoping, and so determines the internal structure of composite classes. Methods in an intersection can access only variables from classes that make up the intersection. There are no free variables that get caught at composition time: the order of nesting around a method definition determines locally which variable an identifier refers to in that method. The same goes for method lookup, when methods are called from the active object -- that is, as a function call, as in "method()", not by sending a message, as in "mary.method()". class Female { data children } class Bovine { class Female { method moo() { if (children > 3) ... } } } In the body of moo(), the inner scope is formed by the intersection of Female and Bovine; the next scope by Female only; and the next by Bovine only. The variable children in the class Female is not in the same definition, but it is in scope. (Here code browser plugins for the editor as mentioned before would be useful.) If the variable children had not been declared in Female, but in Human, moo() would not work, not even for the godess Isis: class Isis = Female Bovine Human The method moo() does not see the variable children in the class Human, although it is present in the composite class Isis. Using the notation class::variable the programmer can explicitly override the scoping order, but not refer to a class outside of the scope. In moo() the distinction between Female::children and Bovine::children could be made, but trying to use Human::children would be an error, as Human is not in scope. To explicitly refer to a variable or method in an intersection, use constructions like Female::Bovine::moo(). (Order is unimportant.) In our moo(), super.moo() will refer to Female::moo() if that exists; if not, Bovine::moo() is tried. Super calls are function calls, not messages. For methods defined only two levels deep, I think everyone except the most quarrelsome will accept this scoping rule as the obvious and "correct" one. The complete rule as currently used in Tingle however is only one of the possible generalizations of the more obvious one for two levels. It is rather complex: it was chosen because it assures that intersections always have precedence over their separate classes, and also that inner scopes have precedence over outer ones. class A { class B { ... class Y { class Z { method myMethod() { print(v) } ... } The variable v will first be looked for in the intersection of all classes A through Z (where it would reside if it was declared immediately above myMethod in the source code), then in the smaller intersections B through Z, C through Z etc. When the intersection Z through Z is reached, i.e. the class Z alone, and nothing has been found, then we continue with the intersections A through Y until only Y, then the intersections A through X until only X etc. This way all continuous intersections (like C+D+E and P+Q+R+S+T) will be searched; intersections are always searched before the single classes that make them up, and also before intersections of only some of the same classes; and inner classes before outer ones. If you want to use a variable or method that is in a non-continuous intersection, you can use the ::-notation; or change the order of nesting for the current method. The latter solution is possible because, as classes can be reopened as often as desired, each method can have its own nesting order; then again, nested blocks provide visible structure to the program, so they should not be chopped up too much. Some taste might be required. This might also be the right moment to remark that when you have to think hard about scoping while reading a program, the program is probably badly written and might benefit from more diverse and more descriptive identifiers. III Programming Style If the previous section leaves you with the impression that scoping for methods many levels deep will be awfully complex, this section is all about avoiding deep nestings. That does help in understanding programs; however the real reason to avoid nesting wherever possible is to promote reusability. (Every nesting is a dependency.) We can emulate boring old single inheritance in Tingle: class car { method start() { ... } method stop() { ... } } class bus { class car { method bus_stop() { ... stop() ... start() ... } } // nothing here -> classical single inheritance } class Car = car class Bus = bus car (There is no such concept as bus-ness that can be combined with the concept "car" to make a bus out of it, so I use "bus" and "Bus" for respectively the base class and the composite class. And "car" versus "Car" is only for symmetry.) This is all very well, but emulating multiple inheritance next would lead to overly deep nesting: note that we only nest with base classes, not with composite classes. (Composite classes have no internal order; and if they would have one, that one would probably not be the appropriate one in all circumstances.) A "literal translation" of code with multiple inheritance would therefore be ugly, and its scoping complex: class carGUIObject { class car { method draw() { ... } } } class busGUIObject { class bus { class carGUIObject { class car { method draw() { ... super.draw() ... } } } } } class CarGUIObject = car carGUIObject class BusGUIObject = bus car busGUIObject carGUIObject There is a better way, with shallow scoping, and all draw() methods snugly together in the source code: class GUIdraw { class car { method draw() { ... } class bus { method draw() { ... super.draw() ... } } } } class CarGUIObject = GUIdraw Car class BusGUIObject = GUIdraw Bus This shows that there is no dominant decomposition by the sort of the vehicle, and also that the "diamond problem" does not exist here. Note that, if we want to add another concern later, we can write this up without taking GUIdraw in scope, when the two concerns are independent of each other: class Accounting { class car { method costs() { ... } class bus { method costs() { ... } } } class employee { ... } } Again, scoping is shallow. We can compose all this with: class CompanyCar = Accounting GUIdraw Car class CompanyBus = Accounting GUIdraw Bus All these good things come with a trade-off: in a system based on intersecting classes, emulating "subclassing for construction" or "subclassing for combination" is the way madness lies. The relationship between a composite class and its parts should always be "is a" or "is" or something similar: never "has a" or "is a bit like a". (Use an instance variable to express "has a" relationships.) ------------- These are the basic principles and some examples of how a language built on them can be used. Ordinary subclasses could be added as syntactic sugar, for instance. There is more at http://dirk.rave.org/tingle/language.txt if you want to read more, but here ends the part about which I would appreciate comments by Real Programmers most. Does this system, in which composition and scoping are decoupled, solve real problems that you have encountered ? Or more to the point; does it solve enough problems to justify the rather heavy scoping rule ? Toy or tool, that is the question... Dirk van Deun -- Ceterum censeo Redmond delendum |