• No results found

4.3 Language Design

4.3.8 Modules

DepJ programs can be flexibly decomposed into modules. Declarations of a class or a method can be distributed over multiple modules. Each module starts with a header specifying the name of the module and the list of modules, on which it depends. For example, Fig. 4.15 shows a possible decomposition of the table widget example, based on the design of Fig. 4.12. Declarations ofTableare distributed into multiple modules, which implement different variations of the table widget and can be used independently from each other. The module TableCore implements the base functionality of table widgets, independent from specific variations, while modules TableSel and TableColResize contain declarations of table widget implementing support for selection and column resizing respectively.

An execution of the DepJ interpreter is parameterized by a main module. The module and its dependencies are loaded and type-checked. Then the method with the namemain

is searched within the loaded modules and executed. Only the declarations of classes and methods from the modules that are directly or indirectly used by the main module are considered by the dynamic dispatch.

Each module is checked in isolation with respect to its dependencies. Only class and method declarations visible within the module, i.e., declared within the module and the used modules, are considered for type checking of the module. In particular, each method call must match a method declaration visible within the module, and each instantiation expression must match a visible class constructor.

As explained in Sec. 4.2.3, the interface of an expression typed by a dependent class is determined by a static dispatch, collecting the declarations of a class matching the statically known types of the class parameters. In the presence of modules, the static dispatch collects only class declarations visible in the module, which ensures that the code depends only in the definitions visible in the modules. By reducing the set of

1 module Widget;

2

3 abstract class Widget {

4 abstract void paint(Graphics g);

5 ...

6 }

1 module TableCore uses Widget

2

3 class Table extends Widget { 4 void paint(Graphics g) { ... }

5 ...

6 }

1 module TableSel uses TableCore

2

3 abstract class SelType { }

4 class SingleSel extends SelType { }

5 ...

6

7 class Table(SelType sel) {

8 abstract boolean isCellSelected(int row, int col);

9 ...

10 } 11

12 class Table(SingleCellSel sel) {

13 boolean isCellSelected(int row, int col) { ... }

14 ...

15 } 16 ...

1 module TableColResize uses TableCore, Bool

2

3 class Table(Bool colResize) { }

4 class Table(True colResize) { ... }

5 class Table(False colResize) { ... }

1 module TableTest uses TableSel, TableColResize

2

3 void main() {

4 Table(sel:SingleCellSel) table = new Table(sel=new SingleCellSel(), colResize = new True()); 5 }

collected class declarations we do not compromise type-safety, because in this way the type-checker can become only more conservative.

On the contrary, dynamic dispatch of a method call or class instantiation expression considers not only the declarations visible within the module where the expression is declared, but also the declarations visible within the main module. In this way, the set of declarations considered by a dynamic dispatch of an expression can be larger than the set of the declarations available for type checking of the expression.

The module system of DepJ guarantees monotonicity of expression typing: an expression that is well-typed within a module M is also well-typed within a module N directly or indirectly using module M; the type of an expression computed in the context of the module M is preserved in the module N. The monotonicity relies on the fact that all declarations visible inM are also visible inN. Thus every method call, constructor call, or field access that is valid inM will also be valid inN.

Method completeness and uniqueness checking is also performed with respect to the declarations visible within a module. A module can be marked as abstract when it is not completely implemented with respect to the visible declarations. In this case, completeness and uniqueness checking is skipped. An abstract module cannot be used as the main module for program execution.

Method completeness and uniqueness checking is not monotonic, though: if method completeness and uniqueness is guaranteed with respect to the declarations visible in the module M, these properties are not necessarily preserved within the dependent module

N. N can introduce new cases for an abstract method declared in M, which can be caused by new class declarations declared in Nor imported from other modules. Thus, completeness and uniqueness of method implementations must be rechecked within the context of each module. Nevertheless, each module declared as non-abstract must be self-consistent.

The modularity of method completeness and uniqueness checking in DepJ is comparable to the modularity of type-checking of classes in languages with multiple inheritance. Each superclass of a classAmust be self-consistent, but there can be conflicting method declarations inherited from different superclasses, thus the inherited declarations must be rechecked for consistency inA, even if they were already checked in the context where they were declared.

An even closer analogy exists between the modules of DepJ and the extensible modules modeled by family classes, which were explained in Sec. 3.4.1.3. A composition of two complete family classes may produce an incomplete family class, thus, completeness of every family class must be rechecked with respect to all inherited virtual classes and their members. An example of such a situation was given in Fig. 3.14.