blob: 681b0a75f9d6f97fb1eaa90c68a884ccef254eea [file] [log] [blame]
See also the example in Test.ngpm
With these definitions, CatamaranSkipper conforms to the Skipper
interface by definition. The "extends" clause would take over the
sailBoat(Boat) operation. Catamaran and Boat have as their only
commonality the conformance with an interface / abstract class that
carries the single method "Skipper getCaptain()" because covariance is
ok for return types. Covariance in this case needs to be defined
further. It probably should mean that the "subtype" offers all
features of the "supertype" in an again conforming way.
Further thoughts on conformance and inference:
An invocation of a signature uses expressions as argument types. For
those, their types can be inferred to a large extent, either because
they are literals or explicitly declared or resulting from other
operation invocations which have a declared type.
Generic types and conformance:
When a generic type is instantiated (or in Haskell-like terminology a
"type constructor" is used to construct a specific type), the
resulting type has a set of signatures. Those signatures are subject
to the regular conformance rules. Therefore, the operations that use
a formal type parameter as the type of an inbound operation argument
will *not* result in conforming signatures when instantiated with
conforming but different types. They would result in covariant but not
contravariant argument definitions.
A conformance check may be required between a ParameterizedClass
(which is not a Class in the metamodel inheritance hierarchy) and a
Class because within the class definition for the ParameterizedClass
the "this" variable may be used. Those usages must be type-checked or
may at least need to a back-propagation of constraints onto the
FormalTypeParameters. It may also turn out that regardless of the type
parameters the use of "this" may never be conforming which then needs
to be reported as an error. For this conformance check, the
FormalTypeParameters need to be included in the recursive conformance
checks. They need to be treates as if they were their typeConstraint
type if that has been specified, or Object (a class with no declared
features at all) otherwise.
Conformance checks are also required between Classes and
ParameterizedClassInstantiations. In this case, there exist a number
of ActualTypeParameters for the FormalTypeParameters used in the
classDefinition of the ParameterizedClass. For the purpose of the
conformance check, these actual types need to be used instead of the
FormalTypeParameter elements themselves. This means that the
conformance check for a ParameterizedClassInstantiation needs to use a
resolver for the FormalTypeParameter elements.
Type adaptation and covariant argument types:
Situation: Type B wants to conform to type A and thus needs to offer
a conforming signature for each of A's signatures. If B wants to
constrain an argument type covariantly in one of the otherwise
conforming signatures, type adaptation may be used to still make B
conforming to A as follows:
class A {
void m(C c) { ... };
class B {
void m(D d) { ... };
Where D is different from C but conforms to C. With these definitions,
B does not conform to A. An adaptation can be provided to make B
conform to A as follows:
adapt B to A {
// feature missing from B to conform to A:
void m(C c) {
if (c instanceof D) {
m((D) c);
} else {
m(/* try some conversion from c into a new object of type D */ ...);
Type inference:
Literals of primitive types should reveal their basic type, probably
not their domain in the sense of length restrictions. For example, if
a variable gets assigned the string "abc" then not necessarily should
there be a length restriction of 3 be imposed on the values assigned
to that variable.
Operations always have a return type associated. That type may itself
have been determined by the type inference built into the language,
but at any given point in (design-)time, that type is known. If not
explicitly declared by the developer, the type may change based on the
implementation of the operation. With this, an expression that invokes
an operation has as its static type the return type of that operation.
VariableExpressions are similar in their typing to an
OperationExpression. If the developer explicitly types the variable,
that defined the static type. Else, the type will be infered from the
program code in which the variable is used. That type is assigned to
the variable and may change during design-time as the developer
modifies the code using the variable.
AssociationNavigationExpressions are statically typed by the type of
the respective association end.
When a not explicitly-typed variable gets assigned a value (or
analogously, a value is returned from an operation, e.g., by assigning
it to the implicit result variable (will we have one?)), the inferred
type of the expression being assigned is added to the inferred set of
possible types for the values of the variable.
Two interesting things can be derived from the inferred set of
possible types: the union and the intersection of all possible
signatures of those types.
If the resulting inferred type of a variable or operation is asked,
the "intersection" is computed over the set of inferred types for
it. The intersection of two DataTypes is another
(anonymous) DataType that exposes a set of signatures computed as the
intersection of the signatures of the respective classes:
context intersection(classSet:Set(Class)):Class
allSignatures(result) = classSet->iterate(
c:Class, result:Set(Operation = allSignatures(classSet->first()) |
In case of an empty result, the variable / operation cannot be
reasonably used in any type-safe manner. This may then be a hint to
the bad practice of a "union type" operation result or variable
assignment and should at least result in a warning.