Tingle Tingle is an object oriented programming language designed around one single innovation: the emancipation of scoping of methods from the composition of classes. Usually, composition determines scoping: subclassing adds new components to a class, which can override the other components and have them in scope; similarly, the order in which mixins are combined creates a hierarchy for scoping; and a bit out of the mainstream, the explicit plumbing used with Smalltalk traits does this in a more flexible manner; but I know of no language in which the roles are reversed, where scoping determines composition. This introduction to Tingle should serve as proof that this is not just a theoretical alternative, but that it can be the basis for a viable programming language. It will show how classes can compose without an explicit order and without explicit plumbing, by just "clicking together" in a way that is determined by the lexical scopes of their individual methods. It will also present the beginnings of a programming style that makes good use of such a language. The first five sections are about programming in the small. Sections I and II each introduce one basic semantic rule for the language; and sections III, IV and V will show how these two rules allow for a novel programming style, as well as for expressing mixins and templates, and emulating good old subclassing. The rest of the text is mainly about programming in the large. In particular, section VI is about how name spaces influence class composition as it was described in section I, and section VII adds to the scoping rule from section II to allow the creation of reusable black boxes. Section VIII adapts the well-known concepts of "public", "private" and "protected" to the language, and section IX adapts Scala companion objects. Section X wraps up with some general remarks. A description of more features of the current prototype version of Tingle can be found at http://dirk.rave.org/tingle/. I The Predominant Method Let us define a simple class Human as follows: class Human { def 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 { def buy_more_shoes() { ... } def 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 { def buy_more_shoes() { ... } def 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 { def buy_more_shoes() { ... } def 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 { def give_birth() { ... } class Human { def 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. More generally, 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 { def 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 { def look_at_passing_trains() { ... } class Female { def 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 one arranges 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()". as Female { var children } as Bovine { as Female { def moo() { if (children > 3) ... } } } (To highlight the semantic difference between class blocks in Tingle and classes in more traditional languages, a block can also be opened with the perhaps more suggestive keyword "as".) 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 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. as A { as B { ... as Y { as Z { def 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. The choice is a matter of taste. This might also be the right moment to remark that when a program makes you think hard about scoping, it might benefit from more diverse and more descriptive identifiers. The scoping rule is primarily designed to make sure that data and methods that you will want to use will very probably be in scope, and that more specialized versions will always be "closer" in scope; not to settle accidental name clashes. 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: as car { def start() { ... } def stop() { ... } } as bus { as car { def bus_stop() { ... stop() ... start() ... } } // nothing here -> equal to classical single inheritance } class Car = car class Bus = bus car The names "car" and "bus" versus "Car" and "Bus" will be used throughout the comparison with single inheritance. Better Tingle style names would for instance be "vehicle" for "car" and "public" for "bus", so that: class Car = vehicle class Bus = public vehicle Anyway, 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: as carGUIObject { as car { def draw() { ... } } } as busGUIObject { as bus { as carGUIObject { as car { def 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: as GUIdraw { as car { def draw() { ... } as bus { def 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: as Accounting { as car { def costs() { ... } as bus { def costs() { ... } } } as 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.) We conclude this section with a rather more superficial example of programming style: a more or less satisfying attempt to write down what in a traditional object oriented language would be subclasses that are smaller than their superclass: as ellipse { var colour, axis as generic { var second_axis def area() { ... } } } as circular { as ellipse { def area() { ... } } } class Ellipse = generic ellipse class Circle = circular ellipse IV Mixins Note again the contrast between the rule from section I to determine the predominant method in a composite class, and the scoping rule from section II. The first is for when an object is accessed "from the outside", with "object.methodname()"; the second is for accesses of methods or variables inside an object, with "methodname()" or "variablename" only. ("Object.variablename" is not allowed in Tingle; if it were, it would be the "predominant variable".) Two distinct rules are needed because order of nesting is only relevant for separate methods; when classes are composed, there is no general order or nesting. "Virtual" function calls, to abuse C++ terminology, as well as mixin- like behaviour can be obtained by "stepping out of the scope" by using "self.methodname()" instead of "methodname()". Note that this "virtuality" here is a property of the call, not of the method itself. (Future versions of Tingle may introduce an annotation to methods that forces all calls to them to this behaviour.) In Tingle there is no technical difference between mixins and classes, but the name mixin would typically fit a class without any intersections, which accesses other methods of the same object through "self" so that it can be mixed with any class that provides the necessary methods. Therefore we recognize the following code snippet as a mixin. It can be mixed with other classes that define isEqual and isLess: as Ordered { def isGreater(x) { ! (self.isEqual(x) || self.isLess(x)) } def isUnequal(x) { ! self.isEqual(x) } } Without nesting, the source code of this mixin does not show what classes it should or should not be mixed with, nor what kind of object "self" will then refer to. As a matter of fact, in the dynamically typed language Tingle, you can mix Ordered with classes that do not provide isEqual and isLess, and so compose broken code. For a very general mixin like this one, all this is not a real problem; but often templates with clean scoping are preferable above mixins. V Templates These are two independent classes, without any intersections: as Tapereader { def read() { ... } } as Diskreader { def read() { ... } } And this is how we extend them together: as Buffered { as Tapereader, Diskreader { def read() { ... super.read() ... } } } class BufferedTapereader = Buffered Tapereader class BufferedDiskreader = Buffered Diskreader Which is short for: as Buffered { as Tapereader { def read() { ... super.read() ... } } as Diskreader { def read() { ... super.read() ... } } } Syntax as used in the examples until here does however not allow to express the following as a template: as Buffered { as Diskreader { as Floppy { def read() { ... super.read() ... } } } as Tapereader { def read() { ... super.read() ... } } } So we add an alternative syntax for nestings that only consist of an intersection: as Buffered { as Diskreader->Floppy { def read() { ... super.read() ... } } as Tapereader { def read() { ... super.read() ... } } } And then we can write the template: as Buffered { as Tapereader, Diskreader->Floppy { def read() { ... super.read() ... } } } As an aside, notice how the arrow syntax is also fit for expressing traditional subclassing concisely: class car->bus { ... } class Bus = bus car This still differs from traditional object oriented languages, as the extra part that is added to the base class is still an independent entity and still gets its own name (bus versus Bus). Still, in a less dynamic version of Tingle, syntactic sugar could easily be added for traditional subclassing, and the name of the extra part could be filled in with a generated unique identifier. Whether every situation will be served well by either mixins or the simple templates described in this section, practice must show. When both are possible, templates seem the cleanest choice. VI Name spaces Although the section on programming style illustrates that nesting of scopes should not become as deep as a classical subclassing programmer would expect, and complexity is thus kept in check, the language as presented up to here does not scale well. This section and the next present two basic features that combine to support programming in the large. The first is simply name spaces. Name spaces can be useful in all kinds of programming languages, but they are especially so in Tingle, where they prevent that classes are reopened without the programmer realizing that they already existed. This is very useful for base classes with very generic names, like indeed the class "generic" from the circles and ellipses example; and in particular if they occur in other people's code. Name spaces make it safe to use composed classes from libraries without detailed knowledge of the entire library. If the library contains base classes with the same names as some of your own classes, they will be distinct by virtue of their name space; so there is no worry about name clashes with the base classes of the composite classes you are using. Name spaces are easy to use correctly: pieces of code that were not designed together should always be in separate name spaces. All class names in a Tingle program are implicitly qualified with the current name space, except if a name space is explicitly specified; so within the default name space "main", any reference to a class named myWidget will be interpreted as myWidget@main: namespace main as myWidget { ... } class MyWidget = Widget@WidgetLibrary myWidget Modules with import en export lists would of course be better than simple name spaces, but the extra advantages of modules over name spaces would be the same as the extra advantages they bring to other languages, so we focus on name spaces here. Using name spaces we can compose classes while keeping their internals completely apart for scoping. VII Your Composite Class is my Base Class Name spaces keep classes apart so thoroughly, that myWidget in the last example could only be a mixin: it cannot access any of the functionality of Widget@WidgetLibrary through scoping, only through self calls. (Assuming that we do not explicitly break through the name space barrier by nesting with fully qualified base class names of the form baseClass@WidgetLibrary, which would require detailed knowledge of the library to do correctly.) When name spaces have cut off all direct access to members of base classes through scoping, all that remains visible is the set of predominant methods of the composed class as a whole. Such sets make up the manageable API of the library. The next step is that we make this API available for scoping again, so that we can for instance emulate subclassing from a composed library class. To enumerate the drawbacks of using only name spaces: we can now use Widget@WidgetLibrary as a black box, which exports its predominant methods as an API, but when using this safe API in the mixin myWidget: - we have to write "self." all the time; - we lose coupling: the source code of the mixin does not specify which one specific other class it is meant as a mixin for (when it is in fact a hack for a subclass); - most importantly, when the mixin and the old class provide methods with the same name, no new predominant method can be added to resolve the conflict, as the classes do not intersect (an important issue if what you actually wanted was a subclass). Therefore we introduce the possibility to regard composite classes as base classes when used outside of their own native name space, and so use them for nesting: as Widget@WidgetLibrary { as myWidget { ... } } class MyWidget = Widget@WidgetLibrary myWidget (To be excruciatingly precise, Widget would actually be allowed to be in the current name space, but not Widget's base classes.) In this example, for the scoping rule, the composite class Widget@WidgetLibrary will be considered as a base class without data members, and with only the predominant methods of the composite. This corresponds with the viewpoint from a mixin, except that: - methods of Widget can be called without "self."; - coupling is explicit: if Widget is not part of a new composite class, then the intersection with myWidget will not be either, and no broken code will be present in the system; - the methods of Widget are less dominant than those in the new intersection with myWidget; - one can also refer to Widget using "super." and the ::-notation to directly access its predominant methods. Composite classes are self-contained for scoping: if they are nested themselves, their methods do not suddenly get the enclosing nestings in scope, as they were not defined in them. (Note that composite classes imported from the same name space can share data; but this is through scoping elsewhere, in the definition of their base classes; so this is the exclusive responsibility of the library writer.) In conclusion, Tingle does not only support a novel very open style in code that belongs and is designed together, but also provides extra hiding and encapsulation when we cross the boundaries of such modules; to fall back to what is essentially multiple inheritance with sharing of common parts. VIII Access control The meaning of such concepts as public, private and protected members can be adapted from languages in the C++ tradition: in Tingle private means "not accessible through scoping" and protected means "not accessible through message sends", which is pretty much what they mean in C++ too. Note that private did not have to imply protected in Tingle; but it seemed to make sense to make this so anyway. Scoped private annotations as in Scala would seem like a perfect fit for Tingle, but they are not yet implemented. In particular "private to the current nesting and everything lexically nested in it" seems promising (although it could use a snappier name). By default all data is protected, i.e. unreachable from outside of the object; and all methods are public. This can be modified using: private var ... protected def ... private def ... The exact meaning of private is: not accessible through scoping from outside of the current base class or the current intersection of base classes. The exact meaning of protected, also as implied by private, is: not accessible for message sends, and also hiding all methods dominated by this one when the predominant method is to be determined. There is a special form of "protected" that is ignored for direct self sends: semiprotected def ... // or shorter: semi def ... Direct self sends are expressions of the syntax "self.msg(...)", as opposed to sends to other expressions that happen to evaluate to be identical to the sending object; and also function calls into composed classes from other name spaces as described in section VII, which are self sends by another name. IX Companion objects The Tingle interpreter allows variable and method declarations at top level, outside of any nesting. These belong to a singleton object named "*lobby*". The class of the object "*lobby*" has no name and is part of no name space. You cannot send messages to the "*lobby*" object (its name is not even a valid identifier). But the members of "*lobby*" are always in scope: the outermost scope, just before the primitive functions. Such top level entities are great for quick experiments at the prompt, but not meant to hold global entities in a program. The clean alternative is companion objects. Each composed class has a companion object associated with it, an object with the same name as the class, which is available in the same outermost scope as top level declarations. Companion objects however reside in the same name space as their associated class, as opposed to top level declarations and primitive functions, that reside in all name spaces at once. Companion objects are explicit containers for methods and values that belong with a class, but not with a specific instance of it; more or less like class methods and class variables in some languages. We need only one extra keyword to declare them: as bankAccount { var amountOfMoney } in bankAccount { var amountOfAccounts } "In" blocks can be nested in other "in" blocks in just the same way as "as" blocks can be nested in other "as" blocks; and as a convenience, they can be nested together in "at" blocks. ("At" blocks can only be nested in other "at" blocks, and can only contain further nestings, no declarations of their own.) Let us say we compose: class BankAccount = bankAccount Then not only is the class BankAccount created out the "as" part of bankAccount; but a second, nameless class (refered to as ~BankAccount in error messages) is created out of the "in" part, and then a singleton object of that latter class is created under the name BankAccount. So there will now be one instance of the variable "amountOfAccounts", part of the object BankAccount; and more in general only one per composed class that includes the base class bankAccount. Companion objects have privileged access to the instances of their associated class; and instances have privileged access to the companion object of their class. Message sends between them are considered equivalent to direct self sends, making semi-protected methods of the companion object accessible to instances, and semi-protected methods of instances accessible to companion objects. The latter property makes companion objects fit as factories: as counter { var value { get, semi set } // generates getter and setter def step() { value = value + 1 } } in counter { def make(|object) { // object is a local variable object = new; // create object of associated class object.set(0); // use semi-protected setter object // return object } } class Counter = counter The factory method is very wordy, so that lots of comments could be added. This is how you would normally write it: in counter { def make() (new).set(0) } You make a new Counter using "Counter.make()". Companion objects can be refered to by their fully qualified name, of the form "Counter@main", or (usually) without the name space component. In the latter case, variables in scope with the same name can have precedence; but the convention of giving composed classes names that start with a capital letter makes this a non-issue. X Vision in Three Paragraphs In class based languages, class names are usually nouns. The system described above invites the programmer to write many classes as adjectives, or concerns; from these, composite classes are constructed, the names of which will again often be nouns. The second step, composition, makes that we need a second mechanism next to scoping to determine which methods are predominant in the composite class; in traditional class based languages these mechanisms can be one and the same, because there is a hierarchy among superclasses and subclasses. I hope that writing concerns as classes can contribute to untangling code conceptually, albeit at the price of more complex scoping mechanics. A good development environment should help the programmer cope with the latter in a way that it cannot do for the former. Dirk van Deun, 2007-2010 (dirk at dinf.vub.ac.be)