4.3 Language Design
4.3.9 Second-Order Dependent Classes
Since dependent classes are parameterized classes, it raises the question how they relate to generics [AFM97, BOSW98, BML97, CS98, OW97]. Both dependent classes and generics have certain advantages and cannot completely encode each other.
First, unlike generics, dependent classes are parameterized by objects and not by types. Although the parameters of dependent classes can also be used in types, they are inter- preted as singleton types, usually used to represent membership of objects in a family. On one hand, the singleton types refer to individual objects and thus cannot be used as a replacement for generic parameters, which abstract from arbitrary object types. On the other hand, generics cannot express membership in families represented by objects, because it does not provide any ways to refer to objects in types.
Second, the interface and the implementation of a dependent class can depend on the type of its parameters, which is expressed by giving different declarations of a class for different parameter types. A generic class has a single declaration, which means that its definition is identical for all possible parameter bindings.
Finally, dependent classes and generic classes differ by the binding time of their param- eters: instances of dependent classes are bound to values computed at runtime, while the parameters of generic classes must be bound statically, and only a completely pa- rameterized generic class can be instantiated. The parameters bound to an object of a dependent class are available as dynamically accessible fields of the object. In this sense, dependent classes are closer to first-class genericity [CS98], which also makes the class parameters available for dynamic use, in particular for dynamic type checks and type casting.
The advantages of generics and dependent classes can be combined, by enabling the parameterization of dependent classes by types. In order to preserve all advantages of dependent classes, in particular the dynamic dispatch, the types must be made available as runtime values, which can be referenced by variables and passed polymorphically during the instantiation of dependent classes. The types of such variables and dependent class parameters are in principle types of types, which we will call second-order types3. The types of objects, introduced in Sec. 4.2.3, will be considered then as first-order types. Analogously, we will refer to the dependent classes parameterized and dispatched by types, as second-order dependent classes.
The benefit of second-order dependent classes is twofold. First, they make it possible to combine generic types with path-dependent types, and hence to combine instances of generic classes and dependent classes in a type-safe manner. This makes it possible to use
3We decided not use the term kind, because it is associated to a specific form of second-order typing in
conventional collection classes such as lists and arrays for storing instances of dependent classes. Second, by replacing generic classes by second-order dependent classes, we enable their specialization for specific types of parameters. For example, we can specify that lists on comparable objects additionally support sorting, which is not available for lists of arbitrary objects.
In Sec. 4.3.9.1 we will take a look at the second-order types supported in DepJ and relationships between them. In Sec. 4.3.9.2 we will take a look at an example of dis- patch by types and analyze its benefits. In Sec. 4.3.9.3 we will discuss combination of parameterization by types and dependent typing and analyze the limitations of such combination in DepJ.
4.3.9.1 Second-Order Types
Parameterization of classes by types in DepJ is supported by allowing types as param- eters of classes. The parameters taking types as values are typed by types of types, which we call second-order types. The language supports three kinds of second-order types corresponding to different sets of first-order types: the set of all first-order types, a singleton set containing only the given type, and a set of types limited by an upper bound, i.e., including all subtypes of the given bound.4
For illustration, consider the implementation of a list collection shown in Fig. 4.16. The given implementation is based on the typical encoding of lists in functional languages: A list is an empty list (Nil) or a list node consisting of a pair of a value and a list
(Cons val list). The listings on the left and on the right contain equivalent implementations of list, but in a slightly different syntax.
At first, let’s focus on the listing on the left of Fig. 4.16, which is based on the primary syntax of dependent classes. The set of all (first-order) types is denoted by keywordtype. For example, on line 1 the class List is declared with parameter T of type type, which means the class can be parameterized by any first-order type. The same holds for its subclasses, Nil(line 3) andCons (line 5).5
A type of the form [<: t] represents the set of all subtypes of the type t, while a type of the form [=t] represents the set containing only the type t. For example, on line 15 we declare variable lst1 with type List([=String]), which means that the parameter T of the list is known to be of type [=String], i.e., a singleton second-order type containing
4The language can be extended with further forms of second-order types, e.g., also supporting lower
bounds, but in the current implementation we limited ourselves to the most common cases, which are sufficient validating the general concept.
5We follow the naming convention to capitalize variables and fields that take types as values. This
1 abstract class List(type T) { }
2
3 class Nil(type T) extends List { }
4
5 class Cons(type T) extends List {
6 Tˆ head;
7 List(T) tail;
8 } 9
10 List(list.T) add(List(type) list, list.Tˆ val) {
11 return new Cons(list.T).init(val, list);
12 }
13
14 List(type) test() {
15 List([=String]) lst1 = new Nil([String]);
16 List([<:String]) lst2 = lst1;
17 lst1 = lst1.add(”a”).add(”b”).add(”c”);
18 // lst2.add("d"); // typing error
19 return lst1;
20 }
1 abstract class List(type T) { }
2
3 class Nil(type T) extends List { }
4
5 class Cons(type T) extends List {
6 Tˆ head;
7 List<T> tail;
8 } 9
10 List<list.T> add(List<?> list, list.Tˆ val) {
11 return new Cons<list.T>.init(val, list);
12 }
13
14 List<?> test() {
15 List<String> lst1 = new Nil<String>;
16 List<? <: String> lst2 = lst1;
17 lst1 = lst1.add(”a”).add(”b”).add(”c”);
18 // lst2.add("d"); // typing error
19 return lst1;
20 }
Figure 4.16: A second-order dependent class for lists
String as the only instance. In other words, we declare that lst1.T is equal to the type
String. In contrast, typeList([<:String]) declares that the precise value of its parameterT
is not statically known – we just know that it is a subtype of String. Thus, we cannot add string values to a variable of this type on line 18.
Type[=String]is a subtype of[<:String], because the set of instances of[=String]consisting of only String is a subset of the set of instances of [<:String], i.e., the set of subtypes of
String. By analogous consideration, although String is a subtype of Object, [=String] is not a subtype of [=Object], but [<:String] is a subtype of [<:Object]. According to the general subtyping rules of dependent classes (See Sec. 5.2.5), List(t) is a subtype of
List(t0), iff t is a subtype of t0. Consequently,List([=String]) is a subtype ofList([<:String])
andList([<:Object]), but not a subtype ofList([=Object]). For example, the assignment on line 16 is type-safe.
Second-order types also produce second-order paths, i.e., paths pointing to values that are not objects, but types. So far we used paths to represent singleton types with the object referenced by the path as the only instance. The same usage of paths is also possible for second-order paths: A second-order path represents a second-order singleton type having the type referenced by the path as the only instance. For example, on line 7 we declare the variable tail of type List(T), which is the same as List(this.T). Hence, the type oftail.Tisthis.T, which means thattailcontains the same type of elements asthis. Another way to use a second-order path is to declare instances of the type referenced by the path. To differentiate between the two usages of a path, we use a different syntax
for the latter case. Namely, we write pˆ to declare instances of the type referenced by the path p. This means that in a list we must write Tˆ or this.Tˆ to declare types of variables referencing elements of the list, e.g., the type of variableheaddeclared on line 6. Declaringhead with type T instead would mean that it is a variable referencing a type, and its value is always equal to the value of the fieldT.
Fields and variables typed by second-order types can be assigned values representing types. A type value can be constructed by a special expression [t], where t is any type valid in the context. For example, on line 15 class Nil is instantiated as an empty list of strings by binding its parameter T to the expression [String], which evaluates to type String. If a type depends on identifiers, they are substituted during evaluation of expression. Thus, type values do not depend on this or local variables. Instead, they may contain references to the corresponding objects in the heap.
Type values can be also a result of evaluating any other expression, e.g., a result of an access to a field referencing a type. For example, in the instantiation expression of line 11 the parameterTof the created object is bound to the result of evaluation oflist.T. If the parameterlist of method add is a list of strings, then list.Twill evaluate to String
and the created object would be again a list of strings.
The listing on the right of Fig. 4.16 shows an alternative syntax supported in DepJ, which is more intuitive to Java developers, and therefore we will use it in most of examples. While in the original syntax we specify all class parameters within simple brackets, alternatively we can specify type parameters in angled brackets using Java-like syntax. In the instantiation expressions, e.g., on lines 11 and 15 of the listing on the right, we can use angled brackets to bind type parameters. We can also use angled brackets to specify the types of type parameters of a class using a Java-like syntax: symbol? is used instead oftype, syntax? <: treplaces[<: t], and instead of writing[= t]
we simply give the type t. For example, on line 15, we declare a list of strings using
List<String>; type List<? <: String> on line 16 refers to a list, whose element type is at leastString; and the return type oftest on line 14 is List<?>, i.e., an arbitrary list. The example demonstrates that second-order dependent classes can be used like generic classes in Java with similar capabilities. Note that these capabilities intensively reuse the base syntax and semantics of dependent classes. The structure of generic types is based on the structure of class types, which already support specification of the types of parameters. References to generic parameters are encoded by path types. We also reuse path equivalence and typing (cf. Sec. 5.2.3) for determining equivalence and upper bounds of the generic parameters. The existing subtyping rules of dependent classes (cf. Sec. 5.2.5) can be used to compare generic types with each other and with the generic parameters.
1 abstract class Serializable {
2 abstract void serialize(Stream output);
3 } 4
5 abstract class List([<:Serializable] T) extends Serializable { }
6
7 class Nil([<:Serializable] T) {
8 void serialize(Stream output) {
9 output.writeString(”[Nil]”);
10 } 11 }
12
13 class Cons([<:Serializable] T) {
14 void serialize(Stream output) {
15 output.writeString(”[Cons]”);
16 head.serialize(output);
17 tail.serialize(output);
18 } 19 }
Figure 4.17: Serialization of lists with serializable elements
4.3.9.2 Dispatch by Types
Second-order dependent classes not only encode generic types, but also enable static and dynamic dispatch by the type parameters. A generic class can be specialized for specific types of its type parameters. For example, the generic definition of a list from Fig. 4.16 can be specialized for specific types of list elements. Fig. 4.17 shows the definition of serialization for lists with serializable elements. Serializable objects are instances of an abstract classSerializabledeclared on line 1 and implement its methodserialize. On line 5 of Fig. 4.17 we refine the generic declaration of List for the case when its elements are instances ofSerializable. In this case,Listis declared to be a subclass ofSerializableand its concrete subclasses must implement serialize. In particular, implementations serializeare given for corresponding refinements of Niland Cons (lines 8 and 14).
The type system determines that the variables of class Cons, head and tail, are both instances ofSerializable, and thus we can safely call method serializeon them on lines 16 and 17. Sincetailis of typeList(T)(See declaration on line 7 of Fig. 4.16), its inheritance fromSerializableis determined by the static dispatch of classListby the statically known type of Tin this context, which is[<:Serializable].
Dynamic dispatch of classes by type parameters makes it possible to instantiate them without static knowledge of their type parameters. For example, lines 1-9 of Fig. 4.18 show a method concatenating two lists. The method is implemented for arbitrary lists assuming only that the lists work with the same types of elements. The result of the method is again a list with the same type of elements as the argument lists. Thus,
1 abstract List<l1.T> concat(List<?> l1, List<l1.T> l2);
2
3 List<l1.T> concat(Nil<?> l1, List<l1.T> l2) {
4 return l2;
5 } 6
7 List<l1.T> concat(Cons<?> l1, List<l1.T> l2) {
8 return new Cons<l1.T>.init(l1.head, concat(l1.tail, l2));
9 } 10
11 class Person extends Serializable {
12 void serialize(Stream output) {
13 output.writeString(”Person”);
14 } 15 } 16
17 void testSerialize(Stream output) {
18 List<Serializable> list1 = new Nil<Person>.add(new Person());
19 List<Serializable> list2 = new Nil<Person>.add(new Person());
20 concat(list1, list2).serialize(output);
21 }
Figure 4.18: Generic list concatenation function, instantiating appropriate lists by means of dynamic dispatch
concatenation of two lists with serializable elements on line 20 is again a list with serial- izable elements, and also an instance ofSerializable. The call of concat on line 20 creates a serializable list due to the dynamic dispatch of thenewexpression on line 8. The class declarations inherited by the created object are determined by the dynamic value ofl1.T. Hence if it evaluates toSerializable or a subtype thereof, the constructed object inherits declarations ofCons and Listfor the element type [<:Serializable].
In a design with Java generics, we would have to define explicit subclasses of Nil and
Cons constraining the element type to a subclass of Serializable and implementing the interfaceSerializable. The problem with explicit subclasses compared to specialization by parameter types is that the generic functionality of lists would ignore the specialization. Although an analogous concat method in Java could be called with serializable lists of parameters, it would still instantiate the classConsand return a list without support for serialization. We could solve the instantiation problem by passing a factory object that instantiates the correct list classes as an additional parameter toconcat. Yet, the return type of concatwould be still a simple list without support for serialization, and the call toserializeon line 20 would require an explicit type cast to theSerializableinterface.
1 class Map(type K, type V) {
2 class Entry {
3 Kˆ key; Vˆ val;
4 }
5 List<Entry> entries;
6 void put(Kˆ key, Vˆ val) { ... }
7 Vˆ get(Kˆ key) { ... }
8 ...
9 }
Figure 4.19: Simple implementation of a map collection in DepJ 4.3.9.3 Parameterization by Dependent Types and its Limitations
As was demonstrated in this section, second-order dependent classes can be used to implement typical generic classes, such as lists and other collections. The advantage of uniform encoding of parameterization of classes by objects and types is that we can freely mix the two kinds of parameterization. The most useful combination scenario is to define of a collection of objects of a certain family via parameterization of collection classes by dependent types. In DepJ a type can depend onthis, on its fields (including references to the enclosing objects), on method parameters and on immutable local variables. Such types can be used as parameters to generic classes. For example, on line 6 in Fig. 4.10 we declared a list of nodes of the same tree by the type expression List<Node(out)>. In that type expression we instantiate the generic definition of lists with the dependent type dependent typeNode(out), which is in turn based on parameterization of classNode
by expression out.
It is, however, difficult to express interdependencies between type parameters of the same class. For illustration, consider a collection implementing a table from keys to values that can be parameterized by the type of the keys and the type of the values. Figure 4.19 shows a typical design of the map collection in DepJ. Class Map is parameterized by the type of its keysK and the type of its values V, and provides methods putand getto associate a value to a key and to get the value associated to the given key. The simplest implementation of a map is based on a list of pairs of keys and values, e.g., using the
Listclass of Fig. 4.16.
As we will see in Sec. 4.4, in certain cases the type of a value assigned to a key may depend on the type of the key. A simple example illustrating the problem is a map taking trees as keys and relating each tree to one of its nodes. In such a map, if we callget method with treet it should return a node of type Node(t). There is no way to define such a map by parameterization ofMapfrom Fig. 4.19, because there is no way to express a dependency of type V on type K. We can derive typeMap<Tree, Node(Tree)>, but such a type allows assigning an arbitrary node to an arbitrary tree.
1 abstract class DependentMap(type K) {
2 abstract class Entry(Kˆ key) {
3 }
4 List<Entry(Kˆ)> entries;
5 void putEntry(Kˆ key, Entry(key) entry) { ... }
6 Entry(key) getEntry(Kˆ key) { ... }
7 ...
8 } 9
10 class TreeNodeMap([=Tree] K) extends DependentMap {
11 class Entry(Kˆ key) {
12 Node(key) val;
13 }
14 void putVal(Kˆ key, Node(key) val) {
15 Entry(key) entry = new Entry(key);
16 entry.val = val;
17 putEntry(key, entry);
18 }
19 Node(key) getVal(Kˆ key) {
20 return getEntry(key).val;
21 } 22 }
Figure 4.20: Encoding dependency of values on keys in a map structure
This said, a type safe implementation of such a map structure is possible, as shown in Fig. 4.20. The reusable part of such a map is extracted to classDependentMap(lines 1-8). As we can see, the class is parameterized only by the type of keys, and the class of its entriesEntry is declared as a dependent class of a key. Instead of assigning a value to a key, the map relates a key to a respective entry.
Concrete subclasses of DependentMap are expected to specialize the type of the key and specify the type of the value to be kept in the map entries. The class TreeNodeMap, defined on lines 10-21, specializes DependentMap for a map from trees to their nodes. It constrains the type of the keys K to Tree and extends the entries of the map with a variable valof type Node(key), which defines the value assigned to the key6. The key of each entry is a Tree, and the value of the entry is a node of that tree. We also define methods putVal and getVal, which serve as convenient wrappers of putEntry or getEntry
allowing clients to work directly with the values of the map, rather than its entries. Note that the classEntryis a class depending on two objects. Dependency on the keyKˆ
is declared by explicit parameterization, while dependency on the map class is expressed by nesting. Dependency on the key enables declaring a variable for storing the value with a type depending on the key, such asNode(key). Dependency on the map enables a type-safe access of the new variable on lines 16 and 20.
6TypesKˆandTreeare equivalent in the contextTreeNodeMap, and we could replace all occurrences