User Tools

Site Tools


Sidebar

Jump to
AmbientTalk
CRIME
iScheme

at:tutorial:modular

This is an old revision of the document!


Modular Programming

This Tutorial is still under heavy construction!

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

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

explain: using import as an object-composition mechanism: concepts behind traits (required/provided interface), aliasing, exclusion.

Classifying objects using stripes

explain: what are stripes? what kind of objects are they, stripe subtyping, stripe test, what default stripes exist

Exception Handling

explain: raise, try-catch and variants, first-class handlers, role of stripes, interface of an exception object

at/tutorial/modular.1177424710.txt.gz · Last modified: 2007/04/24 17:26 (external edit)