Tingle Prototype Features At version 0.7, Tingle is a great language if you are really into doing integer arithmetic and string concatenation. Extra data types and primitive operations should be easy to add but will have to wait until version 1.0. The 0.x versions focus on development of the language core. Version 0.7 implements all the features described in language.txt sections I to VII, except name spaces, only needed for larger programs; and also provides some extras, described below. You will find a temporary, very sketchy language reference in part III. I Extras: recomposing classes As stated in section VII of language.txt, 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. 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. II 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. 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 consequently we can construct a stream of fibonacci numbers to demonstrate the use (or abuse) of 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 { data value { getValue, setValue } = 1 data prev { getPrev, setPrev } = (new node).setValue(0) data next { getNext, setNext } = (new node).setPrev(self) .setValue(prev.getValue()+value) } // identifiers between braces are the names for automatically // generated accessors data fib = new node while True { print(fib.getValue(), "\n"); fib = fib.getNext() } III 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 || 2 ++ 2 (concatenation) unary - 1 unary + 1 unary ! 1 == 2 != 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 Methods: About methods we should only add that declaring local variables goes as follows: method ( parameter, parameter, ... | local variable, local variable, ... ) { ... } Data: Generating trivial getters and setters: data data { } data { , } data = initializer data { } = initializer data { , } = 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". 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. Statements are separated by semicolons. Expressions: = . ( ... ) ( ... ) super . ( ... ) self . ( ... ) self = ( ... ) new renew asnew if then if then else while do while ( , ... ) ( , ... ) . ( ... ) Arithmetic expressions can be formed out of these building blocks using classical infix notation. Method sends can be cascaded. Scoping: :: :: :: etc. Comments: C++ style: /* */ and // IV 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. > class rectangle { data side method setSide(val) { side = val } class generic { data secondSide method setSecondSide(val) { secondSide = val } method area() { side * secondSide } } } > class square { class rectangle { method area() { side * side } } } > class Rectangle = generic rectangle Nil > class Square = square rectangle Nil > data 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 "setSecondSide" for class "Square" composed as ["rectangle","square"] > The prototype does not provide an editor or a means to load files, so best copy and paste blocks of more than one line of code from a separate editor in another window. V Demo code: renew class File { data contents = "abcdef" class Open { method read() { print(first(contents), "\n"); contents = rest(contents) } method close() { renew self ClosedFile } method open() { print("error: already open\n") } } class Closed { method read() { print("error: not open\n") } method close() { print("error: not open\n") } method open() { renew self OpenFile } } } class OpenFile = Open File class ClosedFile = Closed File Dirk van Deun, December 2007 (dirk@dinf.vub.ac.be)