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 <name> , ...
      var <name> { <method> } , ...
      var <name> { <method>, <method> } , ...
      var <name> = initializer , ...
      var <name> { <method> } = initializer , ...
      var <name> { <method>, <method> } = 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 <name> = <name> ...   (return value Nil)

   <expression>

   { <statement> ; ... }       (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:

   <name>

   <name> = <expression>

   <name> . <method> ( <expression> ... )

   <method> ( <expression> ... )

   super . <method> ( <expression> ... )

   self . <method> ( <expression> ... )

   self

   <scoping> <name>

   <scoping> <name> = <expression>

   <scoping> <method> ( <expression> ... )

   new <classname>

   new // create a new object of the same class as the current one

   renew <expression> <classname>

   asnew <expression> <classname>

   if <expression> then <statement>

   if <expression> then <statement> else <statement>

   while <expression> <statement>

   do <statement> while <expression>

   <literal>

   ( <expression> , ... )

   ( <expression> , ... ) . <method> ( <expression> ... )

   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:

   ::
   <classname> ::
   <classname> :: <classname> ::  etc.

Name spaces:

   <classname> @ <namespace>
   
   namespace <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 <name>".  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
<Object: Rectangle>
> s = new Square
<Object: 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)
