13.4.3 The implementation module

Our time interface module specifies the names that are visible to clients of our library. It also serves to specify the names that must be defined in our implementation. To prepare to define those names, we create a separate implementation module:

define module time-implementation
  // Interface module
  use time;
  // Substrate modules
  use format-out;
  use dylan;
end module time-implementation;

In the preceding definition, the implementation module uses the time interface module so that it can give definitions to the names that the interface created. The implementation module is also a client module: It is a client of the dylan module, because its definitions use definitions such as define class, <integer>, and * (which are defined by the dylan module of the dylan library); it is also a client of the format-out module, because the say methods are implemented using the format-out function (which is defined in the format-out module of the format-out library).

We can start to envision the time library as shown in Figure 13.3. In a library more complicated than the time library, we might decompose the construction of the library into several implementation modules. For example, we might want to assign the implementation of the <sixty-unit> substrate to another programmer, and to create an interface between that substrate and the rest of the implementation so that work on either side of the interface can proceed in parallel. In that case, we might use the following module definitions:

define module sixty-unit
  // External interface
  use time;
  // Internal interface
  export <sixty-unit>, total-seconds, decode-total-seconds;
  // Substrate module
  use dylan;
end module sixty-unit;
Figure 13.3 Initial time library.

db20im23

define module time-implementation
  // External interface
  use time;
  // Substrate modules
  use sixty-unit;
  use format-out;
  use dylan;
end module time-implementation;

Here, because the sixty-unit module is an internal interface, we forgo the formality of creating a separate implementation module; we simply export the definitions that we expect to be used by other modules within the library. This approach is perhaps a short-sighted one. If later we want the sixty-unit functionality to be available to another library, we will be faced with reorganizing its module definitions (as we shall see in Section 13.8, page 209). Even within a library, it is good practice to organize modules as interface and implementation.

Notice the distinction between the way that we handled the external time interface, and the shortcut we took with sixty-unit. Although the sixty-unit module will define encode-total-seconds, which is part of the time interface, it does not export encode-total-seconds; rather, it uses the time interface module, which created encode-total-seconds (without defining that function). Because sixty-unit uses time, the name encode-total-seconds is the same object in both modules. Effectively, encode-total-seconds is owned by the time module, although it is defined by the sixty-unit module.

This organization of the external interface may appear odd at first, but it reduces duplication that would otherwise have to occur: If sixty-unit exported encode-total-seconds, then, for it to be visible at the interface of the library, either the sixty-unit module would have to be exported from the library as an interface (which export is undesirable, because the sixty-unit module has other exports that are not intended to be visible outside the library), or the time interface module would have to use sixty-unit and to re-export encode-total-seconds. The create clause provides the cleaner solution of allowing a name to be exported from only the one interface module, defined in a separate implementation module (without exposing the implementation module), and used by many client modules.

Dylan requires that all the variables exported via the create clause be defined by some module in the same library; however, they can be defined in any module, and the interface definitions can be spread over several implementation modules. The compiler will verify that the interface is implemented completely, even if its implementation is spread over several modules, by checking when the library is compiled that each created name has a definition.

The sixty-unit module exports the class <sixty-unit>, because time-implementation will subclass that class. The sixty-unit module also exports the generic functions total-seconds, and decode-total-seconds. The export of total-seconds might seem surprising at first, because, in many object-oriented languages, access to a class includes access to all the slots of a class. In Dylan, slots are simply methods on generic functions and names in the module namespace; hence, the functions must be exported if slot access from outside the module is to be allowed. Note that exporting total-seconds allows other modules only to get the current value of the total-seconds slot. To allow other modules also to set the slot value, we would have to export total-seconds-setter. It is not necessary to export the init keyword total-seconds:, which allows the initial value of the slot to be set when objects are created. Keywords, or symbols, all exist in a single global namespace that is separate from module variables.

Comparison with C++: Dylan modules provide access control similar to that provided by the private: and public: keywords in C++ classes, but Dylan access control is done at the module, rather than at the class, level. Dylan has no equivalent to protected: access control, in that a class that subclasses a class from another module does not have access to slots or other generic functions on its superclass from the other module, unless they are explicitly exported from that module.

Dylan does support multiple interfaces, however; different levels of access can be provided by having more than one interface module, each supplying the access needed for the particular interface.

One way to think of Dylan access control in C++ terms is that all definitions in a module are friends of all classes in the module, and the exported definitions of the module are public.

Breaking out the sixty-unit substrate to a separate module creates a slightly more complicated structure to our diagram, as shown in Figure 13.4.

Figure 13.4 Internal modules of time library.

db20im24

In Figure 13.4, we show the definitions of sixty-unit in a separate module. The sixty-unit module is a client of dylan, an interface and implementation of definitions used by time-implementation (that is, time-implementation is a client of sixty-unit), and an implementation of part of the interface created by time.