An Introduction to Tingle


Tingle is the first idea oriented programming language.  As object
oriented programs have classes as their basic components, idea
oriented programs have ideas, named after Platonic Ideas.  Ideas do
not build on other ideas, as classes extend other classes; instead
they are independent, but they click together when used in
combination, without any need for traits-style glue.  Through this
property, they can also serve as classes, as mixins, even as
dynamically activated layers (as in context oriented programming).

The secret sauce is one single innovation: the emancipation of the
lexical scoping of methods from the composition of classes.
Traditionally, composition determines scoping: for instance,
subclassing adds new components to a class, which can override other
components and have them in scope; and similarly, in a language with
mixins, the order in which they are combined creates scoping; but here
the roles are reversed, and scoping determines composition.

This introduction to Tingle should serve as proof that idea oriented
programming is actually doable, as well as possible.  In particular it
should show that Tingle code is neat and concise, and that the
language encourages separation of concerns.  It will therefore present
the beginnings of a programming style that agrees with the language,
while not looking too foreign to object oriented programmers.

The first three sections cover the basics: after we have explained the
difference between ideas and classes in section 1, section 2
introduces the concept of "top method", and section 3 explains scoping
in detail.

The next three sections are about programming in the small: section 4
compares composition with traditional subclassing and gives some
pointers about programming style, section 5 goes into the relationship
between ideas and mixins, and section 6 introduces simple templates.

Then we need to say something about programming in the large.  Section
7 introduces name spaces, which allow us to reuse library components
as black boxes, either in isolation or with mixins.  Section 8 refines
the scoping rule, which additionally allows us to bring library
components in scope as black boxes, so that we can emulate subclassing
again, if we want to.

The next two sections adapt well-known features from other languages
to Tingle.  Section 9 explains how the concepts of "public", "private"
and "protected" fit in, and adds an extra category.  Section 10 adapts
Scala companion objects.  Section 11 wraps up with some general
remarks.

There are two appendices, which apply specifically to Tingle, not to
idea oriented programming in general.  Appendix A demonstrates some
dynamic features of the language.  Appendix B builds on that to show
that ideas can also be used as layers for context oriented
programming. 


1  Classes and Ideas

In an imaginary language, that looks a lot like Tingle, let us define
a simple class Human as follows:

class Human {
   def play_tetris() { ... }
}

Now to derive a subclass Woman, providing some specifically female
skills, we would, perhaps 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.  Staying as close as possible
to Tingle syntax, we would write something like:

class Human {
   class Woman {
      def give_birth() { ... }
      def walk_on_high_heels() { ... }
   }
}

The two new methods are able to see the Human method play_tetris(),
which is in an enclosing scope.

The above code is in fact syntactically valid in Tingle; but it means
that the classes Human and Woman overlap, and that the two new methods
exist in their set 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 give_birth() { ... }
      def walk_on_high_heels() { ... }
   }
}

class Woman = Human Female.

We now have two independent classes, Human and Female, neither of
which is a superclass or a subclass of the other; and therefore we
needed to separately declare the composite class Woman, modeling
beings that are both Human and Female.

To distinguish between these two kinds of classes, we call Human and
Female ideas, and only Woman will be a class: the name "idea"
(imperfectly) reflects Platonic jargon, where "a woman takes part in
the Ideas Human and Female", instead of being Human with subclass
Woman.  Also, to highlight the difference between classes and ideas,
we will preferably open blocks of ideas with the specialized
keyword "as".

Here we should point out, that classes in Tingle are no more than
collections of ideas: they have no properties except the properties
that they derive from the ideas they contain.  For any collection of
ideas, there will be only one way in which they "click together", so
that the declaration of the class Woman did not need any further
annotations or glue code, nor was even the order of Human and Female
relevant.

The next two sections together will explain how this works.


2  The Top Method

Next let us make the idea Female reusable.  As order is irrelevant for
set intersection, we start by swapping Human and Female:

as Female {
   as Human {
      def buy_more_shoes() { ... }
      def give_birth() { ... }
   }
}

We can now express something that we could not express earlier: the
fact that give_birth() applies to many kinds of females, while walking
on high heels is a skill specific to human females:

as Female {
   def give_birth() { ... }
   as Human {
      def walk_on_high_heels() { ... }
   }
}

As might be expected, a more specialized method give_birth() for human
females could be added in the intersection.  It might then involve
playing Tetris (although this seems highly unlikely).

If we would create an object of a class that consists only of the idea
Female, it would provide the basic method give_birth(), and no
walk_on_high_heels() at all.

If we would create an object of the class Woman, on the other hand,
all the most specialized methods would be available.  Methods in the
intersection of Female and Human would be preferred over methods with
the same name in the separate ideas.  For a Woman named mary,
mary.play_tetris() would refer to a method in the intersection if a
method of that name was available there; if not, it would refer
to a method in either Human or Female.

As there is no hierarchic relationship between Human and Female, when
both the ideas Human and Female would happen to define a method with
the same name, and the intersection did not resolve the conflict,
neither version of the method would win out, and it would be an error
to send a message of that name to the object.

More generally, for a class that combines several ideas in which a
same method name occurs several times, the top method, as we will call
it, is the method of that name in the smallest intersection that
encompasses all the ideas and intersections of ideas that define that
method, if such a method exists at that place.  If not, there is no
top method.  ("More specialized than" is not a total order.)

Now let us reuse the idea Female in the animal kingdom:

as 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():

as Bovine {
   def look_at_passing_trains() { ... }
   as 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.
I opened a new block for the idea Female in this example, away from
its original definition: ideas can be reopened as often as needed, as
an outer nesting as well as nested in others.  I could also have
used a new Bovine-block, but instead chose to add to the one from the
previous example.  The two methods that are now inside the same
Bovine-block seem to go well together thematically.

When one arranges methods thematically by reopening ideas several
times, tools can be provided to view the code by complete ideas, or
complete classes, or to see all variables in a certain scope together,
to help understanding.  Such a tool, called the Idea Browser, is still
in the vaporware stage, but it is not intrinsically hard to make one.
Tools for the opposite direction, that could rearrange code of
traditional classes by theme or concern, would be harder to produce.


3  Scoping

Top methods are concerned with the messages sent between objects.  The
mechanism is flexible: different classes that incorporate one same
idea may have different top methods for the same method names.  But at
a lower level, such flexibility would be undesirable.  If we reference
an instance variable in a method in an idea, we want to be able to see
at that level of the source code, which variable of that name we are
referring to.  If this would vary with which classes the idea would
later be made part of, ideas would be very hard to reuse.  (They would
be like mixins, but without even the benefit of a mixing order.)

That is why variable access is regulated by traditional lexical scope.
The same goes for method lookup when methods are called from within
the same object -- that is, as a function call, as in "method()", not
by sending a message, as in "mary.method()" or "self.method()".

In the example in which we swapped Human and Female, the actual
methods were not shown.  But as soon as we look at the method bodies,
the order of nesting ideas does matter.  So section 2 was a bit
misleading, but as it will turn out, this does not matter in practice.
The examples in section 4 will illustrate that you get concise and
coherent blocks of code if you nest in such a manner that the ideas
that prompt more specialized versions of the methods you are working
on are nested deeper.  Which meshes well with lexical scoping.

Methods in an intersection can access only variables from ideas 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.  To revisit our example one last time:

as Female {
   var children
}

as Bovine {
   as Female {
      def moo() { if (children > 3) ... }
   }
}

Around 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 idea Female is not in the same
block, but it is in scope.  (This is where the Idea Browser will come
in handy.)  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() would not see the variable children in the idea
Human, even though it would be present in the class Isis.  So scope
avoids surprises.

Using the notation idea::variable the programmer can explicitly
override the scoping order, but not refer to an idea 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, so they are also resolved through scoping.

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 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 ideas, and
also that inner scopes have precedence over outer ones.  I will
explain it from an abstract example:

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
ideas A through Z (where it would reside if it was declared
in the same nesting as myMethod), then in the smaller intersections B
through Z, C through Z etc.  When the intersection Z through Z is
reached, i.e. the idea 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
ideas that make them up, and also before intersections of only some
of the same ideas; and inner ideas before outer ones.

If you would want to use a variable or method that is in a
non-continuous intersection, you can still use the ::-notation, but I
suspect that this really means you need to restructure your code.


4  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 car {
   as taxi {
      def answer_call() { ... stop() ... start() ... }
   }
}

class Car = car.
class Taxi = taxi car.

This is all very well, but emulating multiple inheritance would lead
to overly deep nesting: note that we only nest with ideas, not with
classes.  (Classes have no internal order; and if they would have one,
that one would probably not always be the appropriate one.)  A
"literal translation" of code with multiple inheritance would
therefore be ugly, and its scoping complex:

as car {
   as carGUIObject {
      def draw() { ... }
   }
}

as car {
   as carGUIObject { // or should taxi come first ?
      as taxi {
         as taxiGUIObject {
            def draw() { ... super.draw() ... }
         }
      }
   }
}

class CarGUIObject = car carGUIObject.
class TaxiGUIObject = taxi car taxiGUIObject carGUIObject.

There is a better way, with shallow scoping, and all draw() methods
snugly together in the source code; you do this by introducing
only one new idea for the concern "drawing":

as GUIdraw {
   as car {
      def draw() { ... }
      as taxi {
         def draw() { ... super.draw() ... }
      }
   }
}

class CarGUIObject = GUIdraw Car.
class TaxiGUIObject = GUIdraw Taxi.  // equivalent to GUIDraw taxi car

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 taxi {
         def costs() { ... }
      }
   }
   as employee {
      ...
   }
}

Again, scoping is shallow.  We can compose all this with:

class CompanyCar = accounting GUIdraw Car.
class CompanyTaxi = accounting GUIdraw Taxi.

All these good things come with a trade-off: in a system based on
intersecting ideas, emulating "subclassing for construction" or
"subclassing for combination" is the way madness lies.  The
relationship between a class and its parts should always be like "is
a" or "is": never "has a" or "is a bit like a".  (Use an instance
variable to express "has a" relationships.)

We conclude this section with another, more superficial example of
programming style: a rather aesthetically pleasing way to write what
in traditional object oriented thinking would be ugly subclasses
that are smaller than their superclass:

class Ellipse = generic ellipse.

as ellipse {
   var colour, axis
   as generic {
      var second_axis
      def area() { ... }
   }
}

Ellipse is written in this manner so that later we can easily and
correctly derive:

class Circle = circular ellipse.

as ellipse {
   as circular {
      def area() { ... }
   }
}


5  Mixins

Note again the contrast between the rule from section 2 to determine
the top method in a class, and the scoping rule from section 3.
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; if it were, it would be
the "top variable".)

"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.

In Tingle there is no technical difference between mixins and ideas,
but the name mixin would typically fit an idea without any
intersections, which accesses other methods of the same object through
"self", so that it can be mixed into any class that provides the
necessary methods.  Therefore we recognize the following code snippet
as a mixin.  It can be mixed into classes that define isEqual and
isLess:

as ordered {
   def isGreater(x) { ! (self.isEqual(x) || self.isLess(x)) }
   def isUnequal(x) { ! self.isEqual(x) }
}

Lacking nesting, the source code of this mixin does not show what
ideas 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 dynamic
language that Tingle is, we can mix Ordered into 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.


6  Templates

These are two independent ideas, 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() ... }
   }
}

Note how the arrow syntax is also suitable for expressing traditional
subclassing concisely:

class car->taxi {
   ...
}

class Taxi = taxi car.


7  Name spaces

The language as presented up to here does not scale to projects that
use libraries or frameworks or in any way reuse code written by
someone else.  We still need to introduce a mechanism by which any
reusable body of code can be used through a small interface, without
exposing all its internals.  A set of classes, which export only their
top methods, could serve as such an API; but the ideas out of which
they consist should be hidden.  Simple name spaces are enough to
arrange this.  We can put a library in a separate name space and refer
to its classes using fully qualified names:

w = new Widget@WidgetLibrary

Now we can use a Widget without worrying whether some of the ideas
that make up the class Widget have the same names as some of our own
ideas: these ideas are in another name space.  (You can actually refer
to ideas by fully qualified names too, at your own risk: name spaces
hide ideas and classes, but do not make them inaccessible to the
determined hacker.  The point is rather that there are no or very few
good reasons to do this.)

We are not limited to using library classes as they are: we can also
extend them with our own mixins:

namespace main.

as myWidget {
   ...
}
class MyWidget = Widget@WidgetLibrary myWidget.

The class MyWidget consists of all the ideas of Widget plus our own
idea myWidget.  If one of the ideas out of which Widget is composed
were also named myWidget, it would still be separate from our own
myWidget, as it lives in the same class but in a different name space.

What we cannot do, is nest myWidget inside the ideas that make up
Widget, as we are not supposed to know about them.  So myWidget is
necessarily a mixin.  The next section will present a new way to use
nesting, which alleviates this problem without throwing away the
benefits of name spaces.  First two final remarks about name spaces.

One, name spaces are easy to use correctly: pieces of code that were
not designed together should always be in separate name spaces.  All
class and idea 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
an idea named myWidget will be interpreted as myWidget@main.

Two, you might wonder whether name spaces might be equivalent to an
extra idea around all the code that is in them.  They are not, because
they do not exist in isolation.  They have no content of their own and
you have no choice whether to include them in a class or not.
Instead, name spaces can be thought of as simply a mechanism for
rewriting idea and class names, adding suffixes like "@WidgetLibrary".


8  Your Class is my Idea

In the previous section we hid ideas in a name space, and accessed
only classes.  Without access to the ideas, we could not use nesting,
so that we could only use the classes in isolation and with mixins.
Our next step is that we make the API that is offered by the classes
available for scoping again, so that we can emulate subclassing from a
library class, for instance.

To enumerate the drawbacks of using only name spaces: we can now use
Widget@WidgetLibrary as a black box, which exports its top 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 top 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 classes as ideas 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 ideas.)

In this example, for the scoping rule, the class Widget@WidgetLibrary
will be considered to be an idea without data members, and with as
methods only the top methods of the class.  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 newly composed
  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 further from the "top" than those in the
  new intersection with myWidget;
- one can also refer to Widget using "super." and the ::-notation to
  directly access its top methods.

The analogy between ideas from your own name space and classes from
other name spaces is not perfect.  Classes used as ideas 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 classes imported from the same
name space can share data; but this is done through scoping in that
other name space, so this is the responsibility of the library writer,
who should know that code intimately.)

Each nesting should contain at least one "real" idea of your own.
This is not currently enforced, but for instance the effect of adding
code directly in a class used as an idea is left unspecified:

as Widget@WidgetLibrary {
   // the semantics of adding code here has not been settled
}

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.


9  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 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 idea or the current intersection of ideas.

The exact meaning of protected, also as implied by private, is: not
accessible for message sends, and also hiding all methods "under" this
one when the top 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 the calling of top methods
through scoping as described in section 8, which are self sends by
another name.


10  Companion Objects

The Tingle interpreter allows variable and method declarations at top
level, outside of any nesting.  These are stored in a singleton object
named "*lobby*".  The class of the object "*lobby*" has no name and is
part of no name space.  The "*lobby*" object responds to no messages.
But the contents of "*lobby*" are always in scope: the outermost
scope, just before the companion objects and 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.  For the latter
purpose, companion objects are the clean alternative.  Each class has
a companion object associated with it: an object with the same name as
the class, accessed through a scope even outside of that of the top
level variables.  As opposed to top level declarations, which like the
primitive functions reside in all name spaces at once, companion
objects do reside in the name space of their associated class.

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
generally only one per class that includes the idea 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.

Note that "new" without a class name as an argument, meaning "make an
object of the associated class", can avoid the need for slightly
different make methods for several classes that an idea is part of.
(When used in instance methods, it means "make another object of this
class".)

This factory method was 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 classes names that start with
a capital letter makes this a non-issue.


11  Conclusion

In class based languages, class names are usually nouns.  The system
described above invites the programmer to write many ideas as
adjectives, or concerns; from these, classes are constructed, the
names of which will often again be nouns.

The second step, composition, makes that we need a second mechanism
next to scoping to determine which methods are exported by the
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 ideas 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.


Appendices


A  Dynamic Features

Tingle is not only an idea oriented language, it is also a dynamic
language.  It allows us to manipulate classes at run time, for
instance using the operators "renew" and "asnew".

The former changes the class of a single object ("become").  We can do
this when someone 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 former Student object intact, but the
variables from the idea student and from the intersection of the ideas
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.

This, by the way, is why Tingle has no constructors.  Section 10
showed how to use companion objects as factories, but when you allow
objects to be created piece by piece, neither constructors nor
factories are satisfactory solutions.  So instead of constructors,
Tingle provides initializers: each individual instance variable can
have any expression as its initial value.  Such an expression is
evaluated the first time when the variable is read (except if it is
written to first).  This involves a big trade-off though, as these
expressions cannot receive parameters like constructors can; so
companion objects as factories have to supplement them.

Actually, if John takes a job at the university immediately after
graduating, chances are that he will additionally enroll as a Ph.D.
student.  So we might compose a class out of the idea person, plus the
idea student, plus the idea personnel.  But this means that we might
have to solve conflicts between top 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 the idea 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.)

It is interesting to consider what would happen if a method were to
renew self, because the answer follows directly from the principles of
idea orientation.  As long as the new class after renew provides a set
of top methods compatible with the old class (i.e. the same external
API for the object), nothing dramatic will happen.  The method itself
will continue, will be able to access its variables and call other
methods in scope, even if they belong to ideas that are not present in
the new class, and the same goes for earlier methods waiting on the
call stack to continue later.  The object will only behave according
to its new class when it receives a new message, either from itself or
from another object; and switch back to its old behaviour when
returning to methods that were already active before the renew.  The
program logic might get messed up, but at no point is there any danger
of a hard crash.  This is because classes may be fiddled with at run
time, but ideas are immutable.

Now that we know exactly what happens when one object changes class,
we can continue to dynamically activated layers, which will change the
class of many objects at once.


B  Ideas as Layers

Suppose we are making a computer game in which many things work
slightly different when it is night in the game.  We can handle this
by sprinkling the code with "if night then ... else ...".
Alternatively, can make an idea atNight and concentrate all the
differences there:

as atNight {
   as drunk() {
      // adapt behaviour methods to get into more fights
   }
   as hyena() {
      // adapt to howl more
   }
   as ...
}

Now drunks and drunks at night are the same individuals, so it is not
convenient to use the new idea atNight through extra classes:

class Drunk = drunk.
class DrunkAtNight = drunk atNight.

Using such pairs of classes, we could of course do a "renew" for all
the drunks in the game when night falls and another one when day
breaks, and the same for hyenas and all other affected classes, but
that would be tedious.

So instead of using atNight as a component for classes, we activate it
as a layer.  To do some next step in the game under night-time
conditions, we do:

with atNight { next_step() }

For the duration of next_step(), all classes in the system will be
temporarily composed with the idea atNight.  For many classes, this
will of course have no effect at all.  But the behaviour of drunks and
hyenas etc. will change.  (In principle, and as opposed to the effect
of "renew" and "asnew", this change only happens for the current
thread, but as Tingle currently does not provide threads at all, that
is hard to demonstrate.)

Activating layers is a mechanism used in context oriented programming.
When applied in idea oriented programming, it changes the meaning of
message sends only: it has no influence at all on calls through scope,
or on variable accesses.  So the next_step() in the example is
guaranteed to be the same next_step() that would be called at the same
place if its call were not wrapped in a layer activation.  If that is
not the intended effect, we will need self.next_step().


Dirk van Deun, 2007-2012 (dirk at dinf.vub.ac.be)
