A type variable is an identifier used as a type in the context of a generic class definition, generic interface definition or generic method definition.
A type variable is declared in a type parameter as follows.
Syntax
TypeVariable:
(declaredCovariant?='out' | declaredContravariant?='in')?
name=IdentifierOrThis ('extends' declaredUpperBound=TypeRef)?
;
Example 8. Type Variable as Upper Bound
Note that type variables are also interpreted as types.
Thus, the upper bound of a type variable may be a type variable as shown in the following snippet:
class G<T> {
<X extends T> foo(x: X): void { }
}
Properties
A type parameter defines a type variable, which type may be constrained with an upper bound.
Properties of TypeVariable
:
-
-
Type variable, as type variable contains only an identifier, we use type parameter instead of type variable (and vice versa) if the correct element is clear from the context.
-
-
Upper bound of the concrete type being bound to this type variable, i.e. a super class.
Semantics
-
Enum is not a valid metatype in .
-
Wildcards are not valid in .
-
Primitives are not valid in .
-
Type variables are valid in .
A type variable can be used in any type expression contained in the generic class, generic interface, or generic function / method definition.
Example 9. F bounded quantification
Using a type variable in the upper bound reference may lead to recursive definition.
class Chain<C extends Chain<C, T>, T> {
next() : C { return null; }
m() : T { return null; }
}
Type Inference
In many cases, type variables are not directly used in subtype relations as they are substituted with the concrete types specified by some type arguments.
In these cases, the ordinary subtype rules apply without change.
However, there are other cases in which type variables cannot be substituted:
-
Inside a generic declaration.
-
If the generic type is used as raw type.
-
If a generic function / method is called without type arguments and without the possibility to infer the type from the context.
In these cases, an unbound type variable may appear on one or both sides of a subtype relation and we require subtype rules that take type variables into account.
It is important to note that while type variables may have a declared upper bound, they cannot be simply replaced with that upper bound and treated like existential types.
The following example illustrates this:
Example 10. Type variables vs. existential types
class A {}
class B extends A {}
class C extends B {}
class G<T> {}
class X<T extends A, S extends B> {
m(): void {
// plain type variables:
var t: T;
var s: S;
t = s; // ERROR: "S is not a subtype of T." at "s" (1)
// existential types:
var ga: G<? extends A>;
var gb: G<? extends B>;
ga = gb; (2)
}
}
1 |
Even though the upper bound of S is a subtype of T ’s upper bound (since ), we cannot infer that S is a subtype of T ,
because there are valid concrete bindings for which this would not be true: for example, if T were bound to C and S to B . |
2 |
This differs from existential types (see ga and gb and line 21):
G<? extends B> G<? extends A> ). |
We thus have to define subtype rules for type variables, taking the declared upper bound into account.
If we have a subtype relation in which a type variable appears on one or both sides, we distinguish the following cases:
-
If we have type variables on both sides: the result is true if and only if there is the identical type variable on both sides.
-
If we have a type variable on the left side and no type variable on the right side:
the result is true if and only if the type variable on the left has one or more declared upper bounds.
This is the case for
in which T is an unbound type variable and A, B two classes with .
-
In all other cases the result is false.
This includes cases such as
which is always false, even if or
which is always false, even if .
We thus obtain the following defintion:
For two types of which at least one is a type variable, we define
References to generic types (cf. Classes) can be parameterized with type arguments.
A type reference with type arguments is called parameterized type.
Syntax
ParameterizedTypeRef:
ParameterizedTypeRefNominal | ParameterizedTypeRefStructural;
ParameterizedTypeRefNominal:
declaredType=[Type|TypeReferenceName]
(=> '<' typeArgs+=TypeArgument (',' typeArgs+=TypeArgument)* '>')?;
ParameterizedTypeRefStructural:
definedTypingStrategy=TypingStrategyUseSiteOperator
declaredType=[Type|TypeReferenceName]
(=>'<' typeArgs+=TypeArgument (',' typeArgs+=TypeArgument)* '>')?
('with' TStructMemberList)?;
TypeArgument returns TypeArgument:
Wildcard | TypeRef;
Wildcard returns Wildcard:
'?'
(
'extends' declaredUpperBound=TypeRef
| 'super' declaredLowerBound=TypeRef
)?
;
Properties
Properties of parameterized type references (nominal or structural):
declaredType
-
Referenced type by type reference name (either the simple name or a qualified name, e.g. in case of namespace imports).
typeArgs
-
The type arguments, may be empty.
definedTypingStrategy
-
Typing strategy, by default nominal, see Structural Typing for details
structuralMembers
-
in case of structural typing, reference can add additional members to the structural type, see Structural Typing for details.
importSpec
-
The ImportSpecifier
, may be null if this is a local type reference.
Note that this may be a NamedImportSpecifier
. See Import Statement for details for details.
moduleWideName
-
Returns simple name of type, that is either the simple name as declared, or the alias in case of an imported type with alias in the import statement.
Semantics
The main purpose of a parameterized type reference is to simply refer to the declared type.
If the declared type is a generic type, the parameterized type references defines a substitution of the type parameters of a generic type with actual type arguments.
A type argument can either be a concrete type, a wildcard or a type variable declared in the surrounding generic declaration.
The actual type arguments must conform to the type parameters so that code referencing the generic type parameters is still valid.
For a given parameterized type reference with , the following constraints must hold:
We define type erasure similar to Java [Gosling12a(p.S4.6)] as 'mapping from types (possibly including parameterized types and type variables)
to types (that are never parameterized types or type variables)'. We write o
for the erasure of type T.
A parameterized type reference defines a parameterized type T, in which all type parameters of are substituted with the actual values of the type arguments.
We call the type , in which all type parameters of are ignored, the raw type or erasure of T.
We define for types in general:
-
The erasure o of a parameterized type is simply .
-
The erasure of a type variable is the erasure of its upper bound.
-
The erasure of any other type is the type itself.
This concept of type erasure is purely defined for specification purposes.
It is not to be confused with the real
type erasure which takes place at runtime, in which almost no types (except primitive types) are available.
That is, the type reference in var G<string> gs;
actually defines a type G<string>
, so that .
It may reference a type defined by a class declaration class G<T>
.
It is important that the type G<string>
is different from G<T>
.
If a parameterized type reference has no type arguments, then it is similar to the declared type.
That is, if (and only if) .
In the following, we do not distinguish between parameter type reference and parameter type – they are both two sides of the same coin.
Example 11. Raw Types
In Java, due to backward compatibility (generics were only introduced in Java 1.5), it is possible to use raw types in which we refer to a generic type without specifying any type arguments.
This is not possible in N4JS, as there is no unique interpretation of the type in that case as shown in the following example.
Given the following declarations:
class A{}
class B extends A{}
class G<T extends A> { t: T; }
var g: G;
In this case, variable g
refers to the raw type G
.
This is forbidden in N4JS, because two interpretations are possible:
-
g
is of type G<? extends>
-
g
is of type G<A>
In the first case, an existential type would be created, and g.t = new A();
must fail.
In the second case, g = new G<B>();
must fail.
In Java, both assignments work with raw types, which is not really safe.
To avoid problems due to different interpretations, usage of raw types
is not allowed in N4JS.
Calls to generic functions and methods can also be parameterized, this is described in Function Calls.
Note that invocation of generic functions or methods does not need to be parameterized.
We define type conformance for non-primitive type references as follows:
-
For two non-parameterized types and ,
-
For two parameterized types and
Example 12. Subtyping with parameterized types
Let classes A, B, and C are defined as in the chapter beginning ().
The following subtype relations are evaluated as indicated:
G<A> <: G<B> -> false
G<B> <: G<A> -> false
G<A> <: G<A> -> true
G<A> <: G<?> -> true
G<? extends A> <: G<? extends A> -> true
G<? super A> <: G<? super A> -> true
G<? extends A> <: G<? extends B> -> false
G<? extends B> <: G<? extends A> -> true
G<? super A> <: G<? super B> -> true
G<? super B> <: G<? super A> -> false
G<? extends A> <: G<A> -> false
G<A> <: G<? extends A> -> true
G<? super A> <: G<A> -> false
G<A> <: G<? super A> -> true
G<? super A> <: G<? extends A> -> false
G<? extends A> <: G<? super A> -> false
G<?> <: G<? super A> -> false
G<? super A> <: G<?> -> true
G<?> <: G<? extends A> -> false
G<? extends A> <: G<?> -> true
Figure 3. Cheat Sheet: Subtype Relation of Parameterized Types
Example 13. Subtyping between different generic types
Let classes and be two generic classes where:
class G<T> {}
class H<T> extends G<T> {}
Given a simple, non-parameterized class A, the following
subtype relations are evaluated as indicated:
G<A> <: G<A> -> true
H<A> <: G<A> -> true
G<A> <: H<A> -> false
Type Inference
Type inference for parameterized types uses the concept of existential types (in Java, a slightly modified version called capture conversion is implemented).
The general concept for checking type conformance and inferring types for generic and parameterized types is described in [Igarashi01a] for Featherweight Java with Generics.
The concept of existential types with wildcard capture (a special kind of existential type) is published in [Torgersen05a], further developed in [Cameron08b] (further developed in [Cameron09a] [Summers10a], also see [Wehr08a] for a similar approach).
The key feature of the Java generic wildcard handling is called capture conversion, described in [Gosling12a(p.S5.1.10)].
However, there are some slight differences to Java 6 and 7, only with Java 8 similar results can be expected.
All these papers include formal proofs of certain aspects, however even these paper lack proof of other aspect
The idea is quite simple: All unbound wildcards are replaced with freshly created new types ,
fulfilling the constraints defined by the wildcard’s upper and lower bound.
These newly created types are then handled similar to real types during type inference and type conformance validation.
Example 14. Existential Type
The inferred type of a variable
declared as
that is the parameterized type, is an existential type , which is a subtype of A.
If you have another variable declared as
another type is created, which is also a subtype of A.
Note that ! Assuming typical setter or getter in G, e.g. set(T t)
and T get()
, the following code snippet will produce an error:
This is no surprise, as x.get()
actually returns a type , which is not a subtype of .
The upper and lower bound declarations are, of course, still available during type inference for these existential types.
This enables the type inferencer to calculate the join and meet of parameterized types as well.
The join of two parameterized types and is the join of the raw types, this join is then parameterized with the join of the
upper bounds of of type arguments and the meet of the lower bounds of the type arguments.
For all type rules, we assume that the upper and lower bounds of a non-generic type, including type variables,
simply equal the type itself, that is for a given type T, the following constraints hold:
Example 15. Upper and lower bound of parameterized types
Assuming the given classes listed above, the following upper and lower bounds are expected:
G<A> -> upperBound = lowerBound = A
G<? extends A> -> lowerBound = null, upperBound = A
G<? super A> -> lowerBound = A, upperBound = any
G<?> -> lowerBound = null, upperBound = any
This leads to the following expected subtype relations:
(? extends A) <: A -> true
(? super A) <: A -> false
A <: (? extends A) -> false
A <: (? super A) -> true
Note that there is a slight difference to Java: In N4JS it is not possible to use a generic type in a raw fashion, that is to say without specifying any type arguments.
In Java, this is possible due to backwards compatibility with early Java versions in which no generics were supported.
In case an upper bound of a type variable shall consist only of a few members, it seems convenient to use additional structural members,
like on interface I2 in the example Use declared interfaces for lower bounds below.
However, type variables must not be constrained using structural types (see constraint [Req-IDE-76]).
Hence, the recommended solution is to use an explicitly declared interface that uses definition site structural typing for these constraints as an upper bound (see interface in Use declared interfaces for lower bounds).
Example 16. Use declared interfaces for lower bounds
interface I1<T extends any with {prop : int}> { // error
}
interface ~J {
prop : int;
}
interface I2<T extends J> {
}