====== Modular Programming ======
In this tutorial chapter, we introduce AmbientTalk's language features that have to do with writing //modular// programs. The term "modular" can be taken quite broadly in this context. By "modular" programming, we here mean programs hierarchically structured into multiple files and objects composed from multiple so-called "trait" objects. We also describe AmbientTalk's lightweight support for classifying objects and how this enables exception handling based on "types".
===== Namespaces and the lobby =====
In AmbientTalk, programs are primarily partitioned into multiple files. Hence, a file can be regarded as the most coarse-grained module in an AmbientTalk system. Each file has its own namespace: a "global" variable is only globally visible in the file in which it has been defined. There is only one exception to this rule: definitions in the "init" file (which is by default the file ''at/init/init.at'' available in the system library) are considered global to all files.
==== Namespaces ====
In order for AmbientTalk programs to use code defined in other files, a file loading mechanism is required. AmbientTalk provides the concept of a namespace object to perform this task. A namespace object is an object which is internally connected to a directory path on the local file system (how a namespace object is tied to this path will be explained later). By accessing a namespace object's slots, files can be loaded. Consider, for example, a namespace object stored in the variable ''lib'' which is internally tied to the directory ''/home/ambienttalkuser/examples''. Also consider that in the ''examples'' directory, there exists a file named ''math.at'' with the following contents:
object: {
def factorial(n) {
if: (n = 0) then: {
1
} else: {
n*factorial(n-1)
}
};
def fib(n) {
if: (n <= 1) then: {
n
} else: {
fib(n-1)+fib(n-2)
}
};
};
This file can now be loaded by means of the ''lib'' namespace object as follows:
def mathModule := lib.math;
system.println(mathModule.factorial(5));
When the namespace object's ''math'' slot is accessed for the first time, the namespace object checks whether its encapsulated directory contains:
* a file named ''math.at''
* a subdirectory named ''math''
In this example, the file ''math.at'' exists in the directory ''/home/ambienttalkuser/examples''. In this case, the file is loaded and the result of evaluating the code in that file is bound to the ''math'' slot. Recall that the ''math.at'' file defined one object with two methods. The above code snippet shows how these methods can now simply be used in the context of another file.
If the ''math'' slot is accessed a second time, the ''math.at'' file will not be loaded again. Rather, the ''math'' slot simply contains the return value from the loaded code. Hence, namespaces provide the desirable semantics that a file is loaded only the first time it is required. If the ''math.at'' file would not have existed, the call ''lib.math'' may still have succeeded if there would have existed a subdirectory named ''math'' in ''lib'''s directory. In that case, ''lib'''s ''math'' slot would be bound to a new namespace object encapsulating the directory ''/home/ambienttalkuser/examples/math''.
By representing hierarchical directories as nested namespace objects, the AmbientTalk programmer can abstract over the actual files and directories of the underlying file system. For example, imagine that the designer of the ''math.at'' file later decides to change the file structure of his library by creating a directory named ''math'' and by placing the code of each function in a separate file (named ''factorial.at'' and ''fib.at''). The code defined above would not be affected: the expression ''lib.math'' would then evaluate to a namespace object for the ''math'' subdirectory, and the expression ''mathModule.factorial'' would load the file ''factorial.at'' which returns a closure that is immediately invoked.
Obviously, this approach works both ways: if the library designer had started out organizing his project using subdirectories and multiple files, he can always restructure the library's file organization without breaking the interface to clients by replacing a subdirectory with an AmbientTalk file that evaluates to an object whose public slot names correspond to the file names of the original subdirectory.
==== The lobby ====
We have yet to explain how namespaces are initially tied to directories. One possibility is to define a ''root'' variable which would be bound to the "/" directory (i.e. the root directory of the file system). However, to load the contents of the file ''/home/ambienttalkuser/examples/math.at'', this would require writing:
root.home.ambienttalkuser.examples.math
The downside of such as scheme is that the absolute path name of the ''math.at'' file is hardcoded in the AmbientTalk application. If we would later decide to move the file, our AmbientTalk code using that file would be broken! To avoid these problems, AmbientTalk provides the ''lobby'' object. The ''lobby'' is an object defined in the top-level scope whose slots are bound to namespace objects. The names of the slots, and the directory paths of the namespace objects bound to them are not provided in AmbientTalk directly, but rather when starting the AmbientTalk interpreter.
The [[at:tutorial:iat|interactive ambienttalk shell]] (''iat'') has a command-line parameter called the **object path**. This argument is a list of "name=path" entries separated by colons on Mac/Linux or semicolons on Windows. For example, in order to create a namespace object called ''lib'' which is bound to ''/home/ambienttalkuser/examples'', it suffices to start ''iat'' as follows:
iat -o lib=/home/ambienttalkuser/examples
Whenever a new actor is created by the AmbientTalk interpreter, it uses the entries in the object path to initialize the ''lobby'' object of the actor. Hence, by starting up the interpreter as shown above, executing ''lobby.lib'' in any actor returns a reference to a namespace object bound to the ''examples'' directory. This effectively removes absolute path names from AmbientTalk source code and even enables you to quickly reconfigure AmbientTalk code to use other libraries by loading the code using another object path.
Think of AmbientTalk's object path as the equivalent of Java's classpath.
AmbientTalk provides an alias for the ''lobby'' variable named ''/''. Hence, rather than writing ''lobby.lib'', it is also possible to write ''/.lib'' which is often more convenient, although perhaps a bit more cryptic.
==== The current namespace ====
AmbientTalk provides a slot named ''~'' which is always bound to the "current namespace", the namespace in which the current file is contained. This is useful for loading files in a relative manner. For example, in the ''math.at'' file defined above, evaluating ''~'' would result in the ''lib'' namespace. Hence, if there would be a file named ''discretemath.at'' in the directory ''/home/ambienttalkuser/examples'', then the ''math.at'' file could load it by writing:
def discreteMathModule := ~.discretemath;
Loading a module this way is useful because the author of ''math.at'' does not necessarily know of the ''lib'' namespace. Also, using ''lobby.lib'' introduces the implicit requirement that an object path for ''lib'' must be given. The slot ''~'' is always well-defined, so using it does not introduce additional dependencies.
===== Importing objects =====
The previous section has shown how the result of evaluating the content of an external file can be accessed from within another file. One file almost always defines multiple useful function or object definitions. As shown previously, multiple definitions can be returned to clients loading the file by returning an object and making the definitions fields or methods of the returned object. We sometimes refer to such objects as //module objects//, although they are ordinary objects with no special properties.
As shown in the example code above, functionality from a module object can be used simply by accessing its fields or invoking one of its methods. Again, it is not wrong to think of any object as a small kind of module. Sometimes, functionality from a module object is used so often in an importing file that it is worth redefining the function such that it can be accessed unqualified. For example, if a lot of factorials have to be calculated, it is worth defining:
def factorial := mathModule.factorial;
The factorial function has been selectively //imported// into the defining namespace. This pattern of using objects to represent modules and of defining local aliases for a module's functionality to represent a module import can usually take you a long way. However, sometimes it is useful to be able to import //all// functionality provided by an object. In that case it becomes tedious to alias every slot of the imported object. Moreover, if the interface would later change, all of the importing clients would have to be modified as well. To avoid such issues, AmbientTalk provides language support in the form of the ''import'' statement. The ''import'' statement takes an object as an argument and creates local aliases for all of the slots of its argument object. For example:
import mathModule;
is equivalent to:
def factorial := mathModule.factorial;
def fib := mathModule.fib;
When an object has been imported, all of its local slots become lexically visible. This, of course, introduces the danger that previously lexically visible definitions become //shadowed// by the import. Hence, import may accidentally breach the encapsulation of the importing object because the imported object may have defined slots with the same name. If you don't want to run that risk, simply keep the scopes of the importing and imported objects separate and access the module object by means of qualified access as before.
**Warning:** it may seem useful to sometimes be able to import a namespace object. For example, it may seem useful to import the ''lib'' namespace object such that the files ''math.at'' and ''discretemath.at'' become more easily accessible:
import lib;
system.println(math.factorial(5));
However, it is advised not to import namespace objects like this. The reason is that a namespace object is initially empty, and adds slots to itself dynamically whenever a slot corresponding to a file or subdirectory is accessed for the first time. Hence, importing a namespace object often boils down to importing an empty object, obviously not the semantics the programmer had in mind. Loading an entire namespace object is not supported as it would encompass loading all unloaded files in the namespace's directory at once. We feel that using an ''import'' statement to do this is rather implicit.
===== Objects as traits =====
In this section, we describe how the ''import'' statement can also be used to compose multiple objects. In this respect, objects can be regarded as //traits//. [[http://www.iam.unibe.ch/~scg/Research/Traits|Traits]] are a model of object-oriented software composition, similar to (but more advanced than) mixins. A trait can be regarded as a kind of abstract class, which does not define any state, which //provides// a set of methods and which //requires// a set of methods in order to be instantiated. In a class-based language, a class may //use// one or more traits. The class takes on the role of compositor and is responsible for ensuring that, once all traits have been imported, each trait its required interface is provided by the class or other imported traits. A trait is almost literally a piece of code which can be "copy-pasted" into a class.
==== import as trait composition ====
As an example, consider the typical functionality of an "enumerable" object. As the previous sentence made clear, being "enumerable" is a trait: it is an abstract property of an object. Let's define (part of) what behaviour enumerable objects should provide:
def Enumerable := object: {
// map the closure over the collection
def collect: clo {
def result := [];
self.each: { |e| result := result + [clo(e)] };
result
};
// return an element in enumeration for which pred returns true
def detect: pred {
{ |return|
self.each: { |e|
if: pred(e) then: return(e)
};
nil }.escape();
};
// return all elements for which pred returns false
def reject: pred {
def result := [];
self.each: { |e| if: !pred(e) then: { result := result + [e] } };
result
};
}
As can be seen from the method bodies of the ''Enumerable'' object, all implementations rely on the presence at runtime of an ''each:'' method available in the receiver. When viewed as a trait, this enumerable object //provides// the behaviour to collect, detect and reject elements from any enumeration but //requires// that enumeration to implement the ''each:'' method.
Using the ''import'' statement introduced in the previous section, it becomes easy to actually use the ''Enumerable'' object as a trait, by importing it into a composite object:
def Range := object: {
import Enumerable;
def start := 0;
def end := 0;
def init(from,to) {
start := from; end := to;
};
def each: clo {
start.to: end-1 do: clo
};
};
Range.new(0,10).reject: { |i| (i%3 != 0) }
>>[0,3,6,9]
In this example, ''Range'' is a composite object which imports the methods defined by the ''Enumerable'' object. However, how is it possible that ''Enumerable'' uses ''Range'''s definition of ''each:''? This is because the ''import'' statement does not simply alias the binding for methods, but rather defines a //delegate// method for each imported method, as follows:
def Range := object: {
// import Enumerable is translated into:
def collect: clo { Enumerable^collect: clo };
def detect: pred { Enumerable^detect: pred };
def reject: pred { Enumerable^reject: pred };
... // previous definitions from Range
};
So, ''import'' defines small methods which delegate the request to the original trait (using AmbientTalk's support for [[:at:tutorial:objects#first-class_delegation|explicit delegation]]). The use of delegation is crucial here: it means that within the context of the trait, ''self'' is bound to the object using the trait. Hence, when ''reject:'' is invoked on a ''Range'' object, ''Enumerable'''s ''self.each:'' will refer to the correct implementation of ''Range''.
Note that in AmbientTalk, a trait does not need to explicitly specify the set of methods which it "requires" from its composite. However, for documentation purposes, it is often very useful to explicitly state the methods on which a trait depends. One may do so by defining those methods in the trait, and annotating them with the ''@Required'' type tag (which is predefined in the ''lobby.at.lang.types'' module). These methods will //not// be imported when the trait is used by a composite object (otherwise they would cause a conflict with the real implementation methods in the composite). Below is an example of how the ''Enumerable'' trait can be modified to explicitly state its required methods:
def Enumerable := object: {
def collect: clo { /* as before */ };
def detect: pred { /* as before */ };
def reject: pred { /* as before */ };
def each: clo @Required;
}
==== Resolving conflicts: exclusion and aliasing ====
One of the advantages of trait composition is that any conflicts (name clashes) are resolved at trait composition time. Contrast this with e.g. mixin-based composition, where one mixin simply shadows the previous one, which may lead to unexpected behaviour at runtime. The object that imports one or more traits is always responsible for resolving name clashes. A name clash between an already existing slot and a new, imported slot can be solved in two ways: either the imported slot from the trait can be excluded, preferring the existing slot instead or the imported slot can be renamed such that both slots can co-exist in the same object.
The ''import'' statement contains optional ''alias'' and ''exclude'' clauses to allow the importing object to state which slots should not be imported, and which slots should be imported under a different name. The syntax is self-explanatory:
// do not import the slots collect: and detect:
import Enumerable exclude collect:, detect:
// do not import collect: and import reject: as remove:
import Enumerable alias reject: := remove: exclude collect:
If the compositor defines an alias for an imported slot, it is good practice to ensure that the compositor has (or imports) a slot with the original name as well. That way, if the trait object performs a self-send to invoke one of its own methods, it will find a matching slot in the compositor. If the compositor aliases a slot and does not define the slot itself, a lookup by the trait of the original slot name would fail.
''import'' adds two names to the ''exclude'' clause by default:
* ''super'' (the parent object) because this slot is present in //all// AmbientTalk objects.
* ''~'' (the current namespace), because this slot is present in //all// namespace objects.
Moreover, all methods of the trait object annotated with ''@Required'' are automatically excluded upon ''import''.
===== Classifying objects using type tags =====
In class-based languages, classes are a useful tool for categorising objects. Each object is an instance of some class, and sometimes it is useful to be able to ask to which class an object belongs. However, it is well-known by proponents of object-oriented programming that explicitly referring to the class of an object breaks that object's encapsulation. The reason is that a class plays other roles besides object classification. Classes also define an //implementation// for the object's methods. Hence, knowing the class of an object often boils down to knowing the exact implementation of an object, which no longer makes the code polymorphic. This, in turn, means that the code becomes less reusable and more easily breaks when the software evolves over time (because of the addition/removal of classes).
Nevertheless, the ability to classify objects according to //abstract// "types" (we don't use the term "classes" to avoid any confusion) is sometimes useful, especially when the "type-test" cannot be properly refactored into a polymorphic message send. To this end, AmbientTalk provides //type tags//. A //type tag// is simply a brand or marker which can be attached to an object. Type tags are defined using the ''deftype'' keyword. A type tag is identified by its name. For example, we may want to distinguish indexable from non-indexable objects:
deftype Indexable;
Since a type tag is an abstract type or category, it makes sense to define subtype relations on them. Also, since type tags do not carry an implementation, there is no problem in defining a type tag to be a subtype of one or more other type tags. For example, we may want to state that a sortable object is always enumerable as well as ordered:
deftype Enumerable;
deftype Ordered;
deftype Sortable <: Enumerable, Ordered;
When defining an object, the object can be tagged with one or more type tags.
def Array := object: {
...
} taggedAs: [ Indexable, Sortable ]
Finally, the most useful operation defined on type tags is the "type test": it allows objects to test whether an object is tagged with the appropriate type tag.
is: Array taggedAs: Indexable
>> true
is: Array taggedAs: Ordered
>> true
is: Array taggedAs: Set
>> false
The type test determines whether an object //or one of its parents// is tagged with the given type tag //or a subtype// of the type tag.
The type tags with which an object has been tagged can be retrieved by means of the ''tagsOf:'' primitive:
tagsOf: Array
>> [ , ]
Type tags can best be compared to empty Java interface types. Such empty interfaces are sometimes used in Java purely for the purposes of marking an object. Examples are ''java.io.Serializable'' and ''java.lang.Cloneable''. An empty interface type can be implemented by any class (object) and hence only serves the purpose of distinguishing objects by type (by means of ''instanceof'' in Java).
==== Native Type Tags ====
The module ''/.at.lang.types'' shipped with the system library contains the type definitions of the native data types of the interpreter. These type tags can be used to perform type tests on objects, for example:
is: 1 taggedAs: /.at.lang.types.Number
>> true
is: "foo" taggedAs: /.at.lang.types.Text
>> true
The type ''/.at.lang.types.Isolate'' can be used to mark an object as an isolate. Isolate objects are similar to serializable Java objects. They are explained when introducing concurrent programming in [[at:tutorial:actors|a later chapter]].
==== Type tags as annotated message sends ====
In AmbientTalk, messages are objects as well. Quite often, it is useful to tag a message with a type tag that indicates how the message should be processed. AmbientTalk provides syntax for such annotations, as follows:
obj.m(a,b,c)@tag
obj.m(a,b,c)@[tag1, tag2]
In the [[at:tutorial:actors|chapter on actors]] we use this feature to distinguish purely asynchronous message sends from so-called //future-type// message sends. If the message send is annotated with the ''FutureMessage'' type tag, the asynchronous send returns a future, otherwise it returns ''nil''.
===== Exception Handling =====
AmbientTalk employs a traditional exception handling mechanism based on try-blocks. The ''try:catch:using:'' primitive enables the handling of exceptions raised within the dynamic execution path of the try-block. For example:
try: {
calculateSomething();
} catch: DivisionByZero using: { |e|
system.println(e.message);
}
The first argument is a closure to execute, delineating a dynamic piece of code to protect with the given exception handler. The third argument is a one-argument closure, invoked when an exception is caught of the right kind. The second argument is a type tag. By default, exceptions are handled based on their type tags. A hierarchy of type tags is used to classify exceptions. Exception handling in AmbientTalk is quite conventional: when a method is raised, the call stack is unwound until an exception handler installed using ''try:catch:using:'' is found whose type tag argument is a supertype of the raised exception's type. If no matching handlers are found, the exception continues unwinding the call stack until either a handler is found or the stack is entirely unwound. In the latter case, a stack trace is printed.
Raising an exception is done by means of the ''raise:'' primitive:
if: (denominator == 0) then: {
raise: XDivisionByZero.new("division by zero");
}
Note that a new instance of an object named ''XDivisionByZero'' is raised, not the type tag ''DivisionByZero'' itself. An exception is any object that is tagged with (a subtype of) the ''lobby.at.lang.types.Exception'' type tag. By convention, an exception should have the fields ''message'' and ''stackTrace''. The exception module found under ''lobby.at.lang.exceptions'' contains an auxiliary function named ''createException'' which takes a type tag as argument and returns a new prototype exception object. For example:
deftype DivisionByZero <: lobby.at.lang.types.Exception;
def XDivisionByZero := lobby.at.lang.exceptions.createException(DivisionByZero);
==== Exceptions raised by the interpreter ====
The ''lobby.at.exceptions'' module contains predefined prototypes for exceptions that may be raised by the interpreter which are sometimes useful to catch. Examples are:
* ''XArityMismatch'', raised when a function is passed the wrong number of arguments
* ''XParseError'', raised when the parser cannot parse AmbientTalk source code
* ''XSelectorNotFound'', raised when method lookup fails
==== Custom exception handlers ====
You can specify any object to act as a first-class "exception handler" provided that the object understands the following minimal protocol. An exception handler has to implement a method ''canHandle(exc)'' which takes a raised exception object and which should return a boolean value, indicating whether the handler can handle that exception. It also has to implement a method ''handle(exc)'' which allows it to handle an exception for which its ''canHandle'' method returned ''true''. First-class exception handlers are installed by means of the ''try:using:'' primitive. We could rewrite the above "division by zero" example as:
try: {
calculateSomething();
} using: (object: {
// this is a first-class handler object
def canHandle(exc) {
is: exc taggedAs: DivisionByZero
};
def handle(exc) {
system.println(exc.message);
};
} taggedAs: [/.at.lang.types.Handler])
First-class exception handlers are sometimes useful to factor out common exception handling behaviour, and also to specify more complex boolean conditions, e.g. one can express that only ''SelectorNotFound'' exceptions should be caught where the name of the selector that was not found equals ''foo''.
==== Catching multiple kinds of exceptions ====
AmbientTalk defines a number of convenience methods that allow you to list up to three ''catch:using:'' clauses. For example:
try: {
calculateSomething();
} catch: DivisionByZero using: { |e|
system.println(e.message);
} catch: NoSolution using: { |e|
calculateSomethingElse();
};
If more clauses are required, either nested ''try:catch:using:'' calls can be used or the method ''try:usingHandlers:'' can be used. This primitive takes a closure to invoke and a table of first-class handler objects which will be sequentially consulted if an exception is raised during execution of the closure.
Care has to be taken that handlers are listed in increasing order of "generality". If the most general handler (e.g. a handler for ''Exception'') is listed first, other handlers will not have been consulted and will never trigger.
===== Escaping Continuations =====
It is often useful to be able to abort the control flow within a method prematurely. In traditional imperative programming languages, this is done by means of a ''return'' statement (or the ''^'' in Smalltalk). AmbientTalk inherits a similar control structure from the Self programming language. Rather than introducing the notion of a ''return'' statement, AmbientTalk uses the more general concept of an //escaping continuation// to achieve a similar effect. Consider the following code which tests whether a given table ''tbl'' contains an element ''elt'':
def contains(tbl, elt) {
{ |return|
1.to: tbl.length do: { |i|
if: (tbl[i] == elt) then: {
return(true)
}
};
false
}.escape()
}
When ''escape()'' is invoked on a block closure, the effect is to apply that block closure to a special function object (the escaping continuation). That object, named ''return'' in the example, behaves as a function of one argument. When that function is invoked, control immediately returns to the end of the block, and the value passed to the escaping continuation is the result of the ''escape()'' call. If the function object is not called, the closure returns normally and its return value is the result of the ''escape()'' call.
As can be seen, an escaping continuation is more general than a return statement: the continuation is a first-class function object and can hence be passed on to other objects. Hence, it is possible to return from multiple nested function calls at once.
There is an important limitation to the use of escaping continuations. An escaping continuation is not a full-fledged continuation (such as the one provided by Scheme's ''call/cc''): the escaping continuation is valid **only** in the execution of the block closure. Once control has returned from the block (either because it terminated normally with a value, or by means of the escaping continuation), the escaping continuation can no longer be used. If the continuation function object is e.g. stored in a variable and applied later, when the block already terminated, this will result in an exception being raised.