Skip to main content

New site for Dart news and articles

For the latest Dart news, visit our new blog at  https://medium.com/dartlang .

Proposal to eliminate interface declarations from Dart

Posted by Gilad Bracha


Eliminating Interface Declarations from Dart


This document motivates the planned phase-out of interface declarations from the Dart language, and details the required specification changes.

Motivation


In Dart, every class engenders an implicit interface.  Now that this feature is implemented, it is possible to actually eliminate interface declarations from the language. Interface declarations are replaced by purely abstract classes.

Almost every existing interface declaration can already be mapped into an abstract class declaration with no impact whatsoever on other code, by following this formula:

interface I<T> extends J, K  default F<T>{
 set p=(R x);
 R get p;
 U f1;
 final V f2;
 T0 m(T1 a1, ..., Tn An);
 I(S1 p1, ..., Sk pk);
}

maps to

abstract class I’<T> implements J, K {
abstract set p=(R x);
abstract R get p;
abstract set f1=(U _);
abstract U get f1;
abstract V get f2;
abstract T0 m(T1 a1, ..., Tn An);
factory I’(S1 p1, ..., Sk pk){return new F<T>(p1, ..., pk);
}


The implicit interface of I’ is completely equivalent to I in every context.

There are several problems with the above scheme. The most obvious is that it is verbose, mainly due to the extensive use of the abstract modifier. This is easily addressed by eliminating the abstract modifier on methods (respectively getters, setters). If an instance method (getter, setter) has no body, it is deemed to be abstract.

The abstract modifier on the class is unnecessary in this situation as well, so we now have

class I’<T> implements J, K {
set p=(R x);
R get p;
set f1=(U _);
U get f1;
V get f2;
T0 m(T1 a1, ..., Tn An);
factory I(S1 p1, ..., Sk pk){return new F<T>(p1, ..., pk);
}

Beyond the syntax, the following situations are not well supported by the transformation given above:

  1. An interface with a constant constructor cannot be represented.
  2. A generic interface J whose default factory class does not implement J cannot be represented.
  3. Default parameters of the factory constructor need to be repeated, causing a maintenance problem.
  4. The planned support for detecting if a an optional argument was passed by the caller will not serve the factory class’ code.

These are all addressed by the introduction of redirecting factory constructors.



Spec Changes


The changes eliminating the abstract modifier on methods, getters and setters are already in the specification. Various places in the specification need to have any mention of interface declarations removed, but those minor changes are not reflected below.

As always, spec changes (additions and modifications, but not deletions) are highlighted in yellow.



7.6.3 Redirecting Factory Constructors


A redirecting factory constructor specifies a call to a constructor of another class that is to be used whenever the redirecting constructor is called.

redirectingFactoryConstructorSignature:      const? factory  identifier  ('.' identifier)?  formalParameterList `=’ typeName ('.' identifier)?
   ;

Calling a redirecting factory constructor k causes the constructor k’ denoted by typeName (respectively, typeName.identifier) to be called with the actual arguments passed to k, and returns the result of k’ as the result of k.

Note that it is not possible to modify the arguments being passed to  k’. This is a deliberate decision, so that k’ can easily determine what arguments were actually passed by the caller (but we have the same issue with other redirecting constructors, no?).

At first glance, one might think that ordinary factory constructors could simply create instances of other classes and return them, and that redirecting factories are unnecessary. However, redirecting factories have several advantages:
  • An abstract class may provide a constant constructor that utilizes the constant constructor of another class.
  • A factory constructor to which calls are being forwarded can determine whether any user arguments were explicitly passed.
  • A factory constructors avoids the need for forwarders to repeat the default values for formal parameters in their signatures.
  • A generic factory class that aggregates factory constructors for types it does not implement can still have its type arguments passed correctly.

An example of the latter point:

class W<T> implements A<T> { W(w) {...} ...}
class X<T> implements A<T> { X(x) {...} ...}
class Y<T> implements A<T> { Y(y) {...} ...}
class Z<T> implements A<T> { Z(z) {...} ...}


class F<T> { // note that F does not implement A
 static F<T> idw(w) => new  W<T>(w); // illegal - T not in scope in idw
 factory F.idx(x) => new X<T>(x);
 factory F.idy(y) => new Y<T>(y);
 static F idz(z) => new  Z(z); // does not capture the type argument
}

class A<T>{
 factory A.idw(w) => F<T>.idw(w);
// illegal - cannot pass type parameter to static method
 factory A.idx(x) => F<T>.idx(x); // works, but allocates a gratuitous instance of F
 factory A.idy(y) = F<T>.idy; // works
The last two look suspiciously similar;
 factory A.idz(z) => F.idz(z); // wrong - returns Z<Dynamic>; no way to pass type argument
}

It is a compile-time error if k is prefixed with the const modifier but k’ is not a constant constructor.

It is a static warning if the function type of k’ to is not a subtype of the type of k.

This implies that the arguments to k are always legal arguments to k’, and that the resulting object conforms to the interface of the immediately enclosing class of k.

It is a static type warning if any of the type arguments to k’ are not subtypes of the bounds of the corresponding formal type parameters of typeName.



9. Interfaces



An interface defines how one may interact with an object. An interface has methods, getters and setters and a set of superinterfaces.

It is a compile-time error if an interface member m1 overrides an interface member m2 and  m1 has a different number of required parameters than m2. It is a compile-time error if an interface member m1 overrides  an interface member m2 and  m1 does not declare all the named parameters declared by m2 in the same order.

It is a static warning if an interface member m1 overrides an interface member m2 and the type of m1 is not a subtype of the type of m2.  It is a static warning if an interface method m1  overrides an interface method m2,  the signature of m2 explicitly specifies a default value for a formal parameter p and the signature of m1 specifies a different default value for p.


9.1 Superinterfaces

An interface has a set of direct superinterfaces.

An interface J is a superinterface of an interface I iff either J is a direct superinterface of I or J is a superinterface of a direct superinterface of I.

It is a compile-time error if an interface is a superinterface of itself.



Inheritance and Overriding


Let I be the implicit interface of a class C.  I inherits any instance members of its superinterfaces that are not overridden by members declared in C.

However, if there are multiple members m1, …,  mk with the same name n that would be inherited (because identically named members existed in several superinterfaces) then at most one member is inherited. If the static types T1, …,  Tk of the members m1, …,  mk are not identical, then there must be a member mx such that Tx <: Ti, 1 <= x <= k for all  i, 1 <= i <=  k, or a static type warning occurs. The member that is inherited is mx, if it exists; otherwise:
  • If all of m1, …,  mk have the same number r of required parameters and the same set of named parameters s, then I has a method named n, with r required parameters of type Dynamic, named parameters s of type Dynamic and  return type Dynamic.  
  • Otherwise none of the members  m1, …,  mk is inherited.


The only situation where the runtime would be concerned with this would be during reflection if a mirror attempted to obtain the signature of an interface member.

The current solution is a tad complex, but is robust in the face of type annotation changes.  Alternatives: (a) No member is inherited in case of conflict. (b) The first m is selected (based on order of superinterface list) (c) Inherited member chosen at random.  

(a) means that the presence of an inherited member of an interface varies depending on type signatures.  (b) is sensitive to irrelevant details of the declaration and (c) is liable to give unpredictable results between implementations or even between different compilation sessions.

10.10 Instance Creation


Instance creation expressions invoke constructors to produce instances.

It is a compile-time error if any of the type arguments to a constructor of a generic type invoked by a new expression or a constant object expression do not denote types in the enclosing lexical scope. It is a compile-time error if a constructor of a non-generic type invoked by a new expression or a constant object expression is passed any type arguments. It is a compile-time error if a constructor of a generic type with n type parameters invoked by a new expression or a constant object expression is passed m type arguments where m != n.

It is a static type warning if any of the type arguments to a constructor of a generic type G invoked by a new expression or a constant object expression are not subtypes of the bounds of the corresponding formal type parameters of G.



10.10.1 New

The new expression invokes a constructor.

newExpression:
;

Let e be a new expression of the form new T.id(a1, .., an, xn+1: an+1, …, xn+k: an+k) or the form new T(a1, .., an, xn+1: an+1, …, xn+k: an+k). It is a static warning if T is not a class accessible in the current scope, optionally followed by type arguments.

If e is of the form new T.id(a1, .., an, xn+1: an+1, …, xn+k: an+k) it is a static warning if T.id is not the name of a constructor declared by the type T. If e of the form new T(a1, .., an, xn+1: an+1, …, xn+k: an+k) it is a static warning if the type T does not declare a constructor with the same name as the declaration of T.

If T is a parameterized type S<U1, ,.., Um>, let R = S.  It is a compile time error if S is not a generic type with m type parameters. If T is not a parameterized type, let R = T.
Furthermore, if e is of the form new T.id(a1, .., an, xn+1: an+1, …, xn+k: an+k) then let  q be the constructor T.id, otherwise let q be the constructor T. Finally, if R is generic but T is not a parameterized type, then for 1 <= i <= m, let Vi = Dynamic, otherwise let Vi = Ui.  

Evaluation of e proceeds as follows:

If T is not a class or interface accessible in the current scope, a dynamic error occurs. Otherwise, if q is not defined, a NoSuchMethodError is thrown.  Otherwise, if q is a generative constructor (regardless of whether q is redirecting or not), then:

Let Ti be the type parameters of R (if any) and let Bi be the bounds of Ti, 1 <= i <= m. It is a dynamic type error if, in checked mode, Vi is not a subtype of  [V1,  ..., Vm/T1,  ..., Tm]Bi, 1 <= i <= m.

A fresh instance, i,  of class R is allocated. For each instance variable f of i,  if the variable declaration of f has an initializer expression ef, then ef is evaluated to an object of and f is bound to of. Otherwise f is bound to null.  

Observe that this is not in scope in ef. Hence, the initialization cannot depend on other properties of the object being instantiated. Do we want to say that this is not in scope, or that using this is illegal?

Next, the argument list (a1, …, an, xn+1: an+1, …, xn+k: an+k) is evaluated. Then, q is executed with this bound to i, the type parameters (if any) of R bound to the actual type arguments V1, ..., Vm and the formal parameters of q bound to the corresponding actual arguments. The result of the evaluation of e is i.


Otherwise, q is a factory constructor. Then:

Let Ti be the type parameters of R (if any) and let Bi be the bounds of Ti, 1 <= i <= m. In checked mode, it is a dynamic type error if Vi is not a subtype of  [V1,  ..., Vm/T1,  ..., Tm]Bi, 1 <= i <= m.
If q is a redirecting factory constructor of the form T(p1, …, pn+k) = c; or of the form  T.id(p1, …, pn+k) = c; then the result of the evaluation of e is equivalent to evaluating the expression [V1,  ..., Vm/T1,  ..., Tm](new c(a1, …, an, xn+1: an+1, …, xn+k: an+k)).

Otherwise, the argument list (a1, …, an, xn+1: an+1, …, xn+k: an+k) is evaluated. Then, the body of q is executed  with respect to the bindings that resulted from the evaluation of the argument list and the type parameters (if any) of q bound to the actual type arguments V1, ,.., Vm resulting in an object i. The result of the evaluation of e is i.


It is a static warning if q is a constructor of an abstract class and q is not a factory constructor.

The above gives precise meaning to the idea that instantiating an abstract class leads to a warning.  A similar clause applies to constant object creation in the next section.

In particular, a factory constructor can be declared in an abstract class and used safely, as it will either produce a valid instance or lead to a warning inside its own declaration.

The static type of a new expression of either the form new T.id(a1, .., an) or the form new T(a1, .., an) is T. It is a static warning if the static type of ai, 1 <= i <= n+ k may not be assigned to the type of the corresponding formal parameter of the constructor T.id (respectively T).



10.10.2 Const


A constant object expression invokes a constant constructor.

constObjectExpression:
;


Let e be a constant object expression of the form const T.id(a1, .., an, xn+1: an+1, …, xn+k: an+k) or the form const T(a1, .., an, xn+1: an+1, …, xn+k: an+k). It is a compile-time error if T is not a class accessible in the current scope, optionally followed by type arguments.  It is a compile-time error if T includes any type variables.

If e is of the form const T.id(a1, .., an, xn+1: an+1, …, xn+k: an+k) it is a compile-time error if T is not a class accessible in the current scope, optionally followed by type arguments.  It is a compile-time error if T.id is not the name of a constant constructor declared by the type T. If e is of the form const T(a1, .., an, xn+1: an+1, …, xn+k: an+k) it is a compile-time error if the type T does not declare a constant constructor with the same name as the declaration of T.

In all of the above cases, it is a compile-time error if ai, 1 < = i <= n + k, is not a compile-time constant expression.

If T is a parameterized type S<U1, ,.., Um>, let R = S; It is a compile time error if S is not a generic type with m type parameters. If T is not a parameterized type, let R = T.
Finally, if R is generic but T is not a parameterized type, then for 1 <= i <= m, let Vi = Dynamic, otherwise let Vi = Ui.  

Evaluation of e proceeds as follows:

First, if e is of the form const T.id(a1, .., an, xn+1: an+1, …, xn+k: an+k) then let i be the value of the expression new T.id(a1, .., an, xn+1: an+1, …, xn+k: an+k). Otherwise, e must be of the form  const T(a1, .., an, xn+1: an+1, …, xn+k: an+k), in which case let i be the result of evaluating new T(a1, .., an, xn+1: an+1, …, xn+k: an+k) . Then:
  • If during execution of the program, a constant object expression has already evaluated to an instance j of class R with type arguments Vi 1 <= i <= m, then:
    • For each instance variable f of i, let vif be the value of the f in i, and let vjf be the value of the field f in j. If  identical(vif , vjf) for all fields f in i, then the value of e is j, otherwise the value of e is i.
  • Otherwise the value of e is i.

In other words, constant objects are canonicalized.  In order to determine if an object is actually new, one has to compute it; then it can be compared to any cached instances. If an equivalent object exists in the cache, we throw away the newly created object and use the cached one. Objects are equivalent if they have identical fields and identical type arguments. Since the constructor cannot induce any side effects, the execution of the constructor is unobservable.  The constructor need only be executed once per call site, at compile-time.

The static type of a constant object expression of either the form const T.id(a1, .., an) or the form const T(a1, .., an) is T. It is a static warning if the static type of ai, 1 <= i <= n+ k may not be assigned to the type of the corresponding formal parameter of the constructor T.id (respectively T).

It is a compile-time error if evaluation of a constant object results in an uncaught exception being thrown.

To see how such situations might arise, consider the following examples:

class A {
 static final x;
 const A(var p): p = x * 10;
}

const A(“x”); //compile-time error
const A(5); // legal

class IntPair {
 const IntPair(this.x, this.y);
 final int x;
 final int y;
 operator *(v) => new IntPair(x*v, y*v);
}

const A(const IntPair(1, 2)); // compile-time error: illegal in a subtler way

Due to the rules governing constant constructors, evaluating the constructor A() with the argument “x” or the argument const IntPair(1, 2) would cause it to throw an exception, resulting in a compile-time error.

Given an instance creation expression of the form const q(a1, .., an) it is a static warning if T is an  abstract class and q is not a factory constructor.  



12. Libraries and Scripts


A library consists of (a possibly empty) set of imports, and a set of top level declarations. A top level declaration is either a class, a type declaration, a function or a variable declaration.

topLevelDefinition:      classDefinition      | functionTypeAlias    | functionSignature functionBody    | returnType? getOrSet identifier formalParameterList functionBody    | (final | const) type? staticFinalDeclarationList ';'
   |
variableDeclaration ';'
   ;


getOrSet:
 get
| set
;

libraryDefinition:      scriptTag? libraryName import* include* resource* topLevelDefinition*
   ;

scriptTag:
 “#!” (~NEWLINE)* NEWLINE
;

libraryName:
 “#library” “(” stringLiteral “)” “;”
 ;

A library may optionally begin with a script tag, which can be used to identify the interpreter of the script to whatever computing environment the script is embedded in. The script tag must appear before any whitespace or comments.  A script  tag begins with the characters #! and ends at the end of the line. Any characters after #! are ignored by the Dart implementation.

The name of a library can be used for printing and, more generally, reflection. The name may be relevant for further language evolution (such as first class libraries) as well. In the future it may also serve to define a default prefix when importing.

Libraries are units of privacy. A private declaration declared within a library L can only be accessed by code within L. Any attempt to access a private member declaration from outside L will cause a run-time error. Since top level privates are not imported, using them is a compile time error and not an issue here.

The public namespace of library L is the mapping that maps the simple name of each public top level member m of L to m.

The scope of a library L consists of the names introduced of all top level declarations declared in L, and the names added by L's imports.

Libraries may include extralinguistic resources (e.g., audio, video or graphics files)

resource:
 “#resource” “(” stringLiteral “)” “;”
;

It is a compile-time error if the argument x to a library or resource directive is not a compile-time constant, or if x involves string interpolation.


… unchanged …




Interface Types
The implicit interface of class I is a direct supertype of the implicit interface of class J iff:
  • If I is Object, and J has no extends clause.
  • If I is listed in the extends clause of J.
  • If I is listed in the implements clause of J.


A type T is more specific than a type S, written TS,  if one of the following conditions is met:
  1. Reflexivity: T is S.
  2. T is bottom.
  3. S is Dynamic.
  4. Direct supertype: S is a direct supertype of T.
  5. T is a type variable and S is the upper bound of T.
  6. Covariance: T is of the form I<T1, ..., Tn> and S is of the form I<S1, ..., Sn> and Ti  ≪ Si , 1 <= i <= n.
  7. Transitivity: TU and US.

≪ is a partial order on types.
T is a subtype of S, written T <: S, iff [bottom/Dynamic]TS.
Note that <: is not a partial order on types, it is only binary relation on types. This is because <: is not transitive. If it was, the subtype rule would have a cycle. For example:
List <: List<String> and List<int> <: List, but List<int> is not a subtype of List<String>.
Although <: is not a partial order on types, it does contain a partial order, namely ≪. This means that, barring raw types, intuition about classical subtype rules does apply.

S is a supertype of T, written S :> T, iff T is a subtype of S.

The supertypes of an interface are its direct supertypes and their supertypes.

A type T may be assigned to a type S, written  T ⇔ S, iff either T <: S or S <: T.
This rule may surprise readers accustomed to conventional typechecking. The intent of the ⇔ relation is not to ensure that an assignment is correct. Instead, it aims to only flag assignments that are almost certain to be erroneous, without precluding assignments that may work.

For example, assigning a value of static type Object to a variable with static type String, while not guaranteed to be correct, might be fine if the runtime value happens to be a string.