Tingle Prototype Features At version 0.9.1, the core of Tingle is complete and unlikely to change much in the future. Its collection of data types and primitive operations is very limited though, making it a demo version of what a Tingle 1.0 could be. The file language.txt presents an ideal version of the language; and this file prototype.txt documents the quirks and experimental features of the prototype, which are still subject to change. You will also find a rather sketchy language reference in part IV of this document. I Preliminaries: adding code directly into a composite class Although it may not necessarily be a good idea to do so, in the current Tingle prototype you can add code directly into a composite class, that is, as follows: as Widget@WidgetLibrary { as myWidget { ... } def grok_this() { ... super.method() ... } } Scoping: the method grok_this() is in scope for the intersection of Widget and myWidget, and hides any predominant method with the same name in the class Widget. The body of grok_this() itself also only has access to the predominant methods of Widget. Thus super.method() can only refer to a method outside of the scope Widget, which in the example can only mean a globally defined method. Predominant method: it is as if grok_this() is in the intersection of all classes out of which Widget itself is composed. One could expect that if a method with the same name already existed in that intersection, it would be overwritten; and that would be a perfectly good semantics. That is not what happens in Tingle though: it means that two methods with the same name are both present in the same intersection, which of course means that neither can be predominant -- a design decision that supports the feature described in the next section. II Extras: recomposing classes As stated in section I, a method defined directly in a composite class does not overwrite a method with the same name defined in the intersection of all classes that make up that composite class. The reason is that in Tingle, classes can be recomposed out of a new set of composing classes, so that methods that are defined directly in a composite class can "move" to another intersection. (It could be a smart idea not to allow recomposing classes if they are used in nestings; however, the current prototype does allow it.) Tingle has an executable statement "class =" instead of a declaration "class =", so that in the following code snippet, the last line changes the behaviour of the object widget1: class Widget = baseWidget niceBorders widget1 = new Widget class Widget = baseWidget niceBorders funkyColours This should be useful, and could even be made more useful in the form of "class +=" and "class -=", but it makes the semantics of adding methods directly into composite classes a bit ugly. It must be remarked that Tingle does a lot of caching of lookups for efficiency, and that class recomposition currently causes all caches to be invalidated. We will not say much more about this feature, apart from remarking, that recomposing a class at run time does not need to disrupt the functioning of an object that is active at that time, as long as it does not do explicit self calls. Access to variables in particular is completely safe, as the semantics of "class =" in this system is to change predominant method lookup, without any effect on scoping. III Extras: initializers, renew, asnew We cannot only recompose classes, we can also manipulate the class of individual objects, using the operators "renew" and "asnew". The former changes the class of an object ("become"). For instance, when John graduates and proceeds to take a job at his university: class Student = person student class Personnel = person personnel renew john Personnel This leaves all variables of the object intact, but the variables from "student" and from the intersection of "person" and "student" will not be reachable anymore (because no more methods will be defined on the object that have these in scope) until John decides that it was more fun being a student, and changes again. Actually, if John takes a job at the university immediately after graduating, chances are that he will additionally enroll as a Ph.D. student. We might compose a class out of "person" plus "student" plus "personnel" for that case. But maybe this means that we will have to solve conflicts between predominant methods of "Student" and "Personnel", where we would prefer to keep both, for instance getID() for student ID as well as for personnel ID. We can avoid having to fix this if we know that John will in all circumstances act either as a student, or as a member of personnel, but never both at the same time. In that case we can use "asnew" to create an extra role: johnAsPersonnel = asnew john Personnel This way john and johnAsPersonnel will always have the same internal state -- although only the variables of class "person" are really shared; all others are in scope for methods of only one of the roles -- but belong to different classes. (The objects john and johnAsPersonnel are identical for the "==" and "!=" operators.) What to do about constructors when objects are not necessarily created all at once, but possibly by bits and pieces, is another matter. If you are never going to change the classes of objects, then companion objects, having access to semi-protected methods of objects of the classes they correspond to, can serve as factories in the absence of constructors. But if you are, then Tingle provides a simple alternative: every instance variable can have an initializer, an expression that is evaluated when the variable is read for the first time. Such an initializer can be any expression, and will be evaluated in the scope where it is defined; it can be seen as a partial constructor (without parameters though). This means that we have some laziness in the language, and that -- purely to demonstrate the language feature, and in no way implying that this represents a sane programming style -- we can construct a lazy stream of fibonacci numbers using initializers. Note that initializers of variables that are assigned to before the first time they are read from, will never be executed, so that the initializer of "prev" will only ever be executed for the initial node: class node { var value { getValue, setValue } = 1 var prev { getPrev, setPrev } = (new node).setValue(0) var next { getNext, setNext } = (new node).setPrev(self) .setValue(prev.getValue()+value) } var fib = new node while True { print(fib.getValue(), "\n"); fib = fib.getNext() } Also note that lazy initializers can lead to correct-but-unexpected behaviour during interactive sessions. Compare: > var a = 1 > var a = 1 > a 1 > var a = 2 > var a = 2 > a > a 2 1 In these examples, we redeclare a variable and replace not its value, but its lazy initializer. In the transcript to the left, a has not been dereferenced before it was redeclared, so the original initializer has not been evaluated and its value has not been stored as the value of the variable a yet. At the right, it has, and so the new initializer never gets used. (This is all very well for a prototype, but redeclaring variables should of course better be treated as an error.) IV Informal language reference Data types: String, Integer, Boolean, Object, Nil Literals: True, False, Nil Strings take double quotes. Primitive functions: Name Arity print Any read 0 + 2 - 2 * 2 / 2 % 2 (rest after integer division) ++ 2 (concatenation) unary - 1 unary + 1 unary ! 1 == 2 (pointer equality when objects, but see note on asnew) != 2 > 2 >= 2 < 2 <= 2 abs 1 first 1 (first letter of a string) rest 1 length 1 isString 1 isInteger 1 isBoolean 1 isObject 1 isNil 1 toString 1 toInteger 1 companion 1 (returns companion object of the class of an instance) The above functions are the only real functions in the system (as opposed to methods), i.e. only available through scoping. && 2 || 2 The logical and and or are really syntax rather than functions, to support evaluation of their right argument by need. Classes: class x { ... } as x { ... } These are equivalent, but "as" is prefered, in contrast to class composition. Declarations inside a class block may be separated by semicolons, but do not need to be. in x { ... } at x { ... } Methods: About methods we should only note that declaring local variables is done as follows: def ( parameter, parameter, ... | local variable, local variable, ... ) { ... } A method body is a statement, customarily a block statement. Data: Generating trivial getters and setters: var , ... var { } , ... var { , } , ... var = initializer , ... var { } = initializer , ... var { , } = initializer , ... The first method name is for the getter, the second for the setter. If only 1 method name is given, it is a getter. The setter returns "self". Access control: The keyword "var" can be preceded by "private". The keyword "def" can be preceded by "private" or "protected" or "semiprotected"/"semi", and so can getter and setter names. Statements: class = ... (return value Nil) { ; ... } (return value of the last expression or Nil for the empty block) The difference between statements and expressions is syntactical only; statements have a return value just like expressions. The only reason to have a separate category of statements is that allowing the "class =" syntax in expressions would lead to hard to understand expressions. Expressions: = . ( ... ) ( ... ) super . ( ... ) self . ( ... ) self = ( ... ) new new // create a new object of the same class as the current one renew asnew if then if then else while do while ( , ... ) ( , ... ) . ( ... ) Arithmetic expressions can be formed out of these building blocks using the usual infix notation. Method sends can be cascaded. Note that you can use "new" with a single base class in the prototype, meaning: "create an object of an ad hoc composite class composed solely out of this base class". I never bothered to have companions generated for these, though. Scoping: :: :: :: :: etc. Name spaces: @ namespace Comments: C++ style: /* */ and // V Example interaction At the prompt, the prototype accepts class blocks as well as declarations of global data and methods outside of all classes, and also simple statements and expressions; and finally you can change namespaces with "namespace ". Namespace declarations do not go with textual blocks, making them easier to use in a read-eval-print- loop: they stay valid until the next namespace declaration. > as rectangle { var side def setSide(val) { side = val } as generic { var secondSide def setSecondSide(val) { secondSide = val } def area() { side * secondSide } } } > as square { as rectangle { def area() { side * side } } } > class Rectangle = generic rectangle Nil > class Square = square rectangle Nil > var r, s > r = new Rectangle > s = new Square > r.setSide(3) 3 > r.setSecondSide(5) 5 > print(r.area(), "\n") 15 Nil > s.setSide(2) 2 > print(s.area(), "\n") 4 Nil > s.setSecondSide(5) send: no method setSecondSide for class Square composed as square rectangle > The prototype accepts file names as arguments; these files are then read, displayed line by line, and interpreted as if these lines were typed at the command prompt. VI Demo code: renew class File { var contents = "abcdef" class Open { def read() { print(first(contents), "\n"); contents = rest(contents) } def close() { renew self ClosedFile } def open() { print("error: already open\n") } } class Closed { def read() { print("error: not open\n") } def close() { print("error: not open\n") } def open() { renew self OpenFile } } } class OpenFile = Open File class ClosedFile = Closed File VII To do Tingle allows nesting with composite classes without any limitations, in effect totally blurring the difference between base classes and composed classes; which is nice for experimentation, but way too powerful for a practical language: to maintain programmer sanity, we should really require that all the base classes making up a composite class used for scoping are from another name space. The Tingle interpreter does not at this moment check whether composed classes are subtypes of all classes that are made up of a subset of their set of base classes, a property that will only come to its own in a statically typed system. There are several reasons why they would not be now: - composition can "cancel" methods because of an unresolved conflict in determining the predominant version; - a protected or private method can dominate a public one; - methods can be overridden with methods with a different amount of parameters. Modelled after self.method() and super.method(), we could define previous.method(). Because if a class can be reopened several times, it could also be nice to be able to overwrite a method, and call the old version from the new one with previous.method(). A simple application would be tweaking a framework without touching the source code. (Changing a method "in place" by reopening the original scope also affects all methods that have the original one in scope, an effect that cannot be reached in any other way.) As Tingle is not fit for subclassing for construction, it might be nice to introduce some kind of delegation, for instance with instance variables that are declared with "fwd" instead of "var". These could export their predominant methods to the scope that the objects themselves are in. Dirk van Deun, 2007-2010 (dirk at dinf.vub.ac.be)