Well, I've read Schärli's Traits thesis, which is getting uptake in Perl 6 and Fortress [PDF] among other places. And it makes me furiously to think.
Do we really need inheritance (or delegation, which is inheritance at the object level) in a traits-based world? Why bother with overriding methods when they can be just replaced selectively? Sending to super is a marginal feature, and can easily be simulated by selective including and renaming from traits.
Here's my current vision, best if eaten by <date> yada yada:
Code is encapsulated in methods, so we still have methods, but classes go away in favor of two sort-of-new concepts, behaviors and types. A behavior is just a set of methods and associated private state variables. The methods in the behavior can be local (in which case they are not visible outside the behavior, and are basically just subroutines), standard, or abstract. You cannot instantiate a behavior as such, nor can a variable be typed (in a statically typed language) to a behavior. It's just a pile of things that an object might be able to do. If a behavior wants to expose its private state, it does so with a getter and/or setting method; a smart language will make it easy to specify that you want these things.
Types are used to instantiate objects and declare variables. They are composed out of behaviors: the methods available on an object of type T are the non-local methods provided by the behaviors out of which T is composed. Unless the type is itself abstract, any abstract methods in one behavior must be supplied by a standard method in another behavior, a sort of peg-and-hole operation. You can bring in a single method from a behavior, suppress a method in a behavior, or rename a method in a behavior when constructing a type; that allows you to compose behaviors that don't quite fit together perfectly. All this is verified when the type is compiled; a clever IDE can notice discrepancies and warn the programmer about them.
We also need a notion of private vs. public methods, but it's not clear to me whether this should be declared directly on the method (i.e., where the behavior is) or at the type level. That's just notational, however. With that established, we can give an implementation-independent definition of subtypes and supertypes, declared at the type level. The compiler verifies that the methods of a declared {sub,super}type are a {super,sub}set of the type currently being specified, and that arguments are appropriately contravariant and results covariant (in a statically typed language) so as to provide minimum requirements for Liskov substitutability.
I'm not sure yet what the constructor/destructor story might be: I like factory methods better than constructors anyhow, and perhaps that's the Right Thing.
Ideas? Comments? WAGs?
4 comments:
I think one of the key points of traits that makes them simpler than multiple inheritance is that they don't have state. Instead, they declare getters and setters (or whatever), and the class implements the state.
If you put private fields into the traits, you'll just get the diamond inheritance problem all over again. The class needs to decide whether two traits share the same state or not.
In this model, there is no sharing of state between behaviors incorporated into the same type: each behavior takes responsibility for its own portion of the state. They can happen to use the same names for state variables, but that's irrelevant.
See my earlier posting on divisions, which segment classes into groups of methods, each group taking responsibility for its own state. This is essentially the same notion but in reverse: the implementation of a type is the result of composing behaviors.
After all, there's no fundamental difference between referencing a getter/setter pair from elsewhere and referencing state variables directly. Indeed, in Self you can't tell at the point of reference whether you are {getting,setting} a slot or calling a {zero,one}-argument method.
I think the statelessness of the traits model was an accidental result of the fact that Squeak instance variables are referenced by name at runtime.
Interesting. That would certainly work when you don't want to share state (which is already the case with delegation).
It seems like plugging multiple behaviors together to make a class could be quite elegant when behaviors are designed to fit together. But in many cases there's going to be some glue code to adapt them. In a language like Java, two behaviors might be implemented as separate classes, two instance variables would point to them, and the glue code would sit in the class itself. But in a langage where code is disallowed in classes, you'll need to write a glue behavior that probably isn't useful outside of the class it's intended for.
That could be done in this model by allowing the declaration of type-local behavior, which is semantically like any other behavior but whose name is not visible outside the type. It wouldn't even have to have a name, in fact, which would make it easy to write simple single-behavior types.
Post a Comment