An Introduction to the Tingle Programming Language

Dirk van Deun, dirk at dinf.vub.ac.be

Section 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 goddess 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.



Contents | Section 4: Programming Style