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.

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

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

modifier on methods (respectively getters, setters). If an instance method (getter, setter) has no body, it is deemed to be abstract.

modifier. This is easily addressed by eliminating the

There are several problems with the above scheme. The most obvious is that it is verbose, mainly due to the extensive use of the

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:

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.

The planned support for detecting if a an optional argument was passed by the caller will not serve the factory class’ code.

Default parameters of the factory constructor need to be repeated, causing a maintenance problem.

whose default factory class does not implement

An interface with a constant constructor cannot be represented.

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

m

1

overrides an interface member

m

2

and

m

1

has a different number of required parameters than

m

2

. It is a compile-time error if an interface member

m

1

overrides an interface member

m

2

and

m

1

does not declare all the named parameters declared by

m

2

in the same order.

It is a static warning if an interface member

m

1

overrides an interface member

m

2

and the type of

m

1

is not a subtype of the type of

m

2

. It is a static warning if an interface method

m

1

overrides an interface method

m

2

, the signature of

m

2

explicitly specifies a default value for a formal parameter

p

and the signature of

m

1

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

m

1

, …, m

k

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

T

1

, …, T

k

of the members

m

1

, …, m

k

are not identical, then there must be a member

m

x

such that

T

x

<: T

i

, 1 <= x <= k

for all

i, 1 <= i <= k,

or a static type warning occurs. The member that is inherited is

m

x

,

if it exists; otherwise:

If all of m 1 , …, m k 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 m 1 , …, m k 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:

new type ('.' identifier )? arguments

;

Let

e

be a new expression of the form

new

T.id(a

1

, .., a

n

,

x

n+1

: a

n+1

, …, x

n+k

: a

n+k

)

or the form

new

T(a

1

, .., a

n

,

x

n+1

: a

n+1

, …, x

n+k

: a

n+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(a

1

, .., a

n

,

x

n+1

: a

n+1

, …, x

n+k

: a

n+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(a

1

, .., a

n

,

x

n+1

: a

n+1

, …, x

n+k

: a

n+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<U

1

, ,.., U

m

>,

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(a

1

, .., a

n

,

x

n+1

: a

n+1

, …, x

n+k

: a

n+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 V

i

=

Dynamic

, otherwise let

V

i

= U

i

.

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:

L et T i be the type parameters of R (if any) and let B i be the bounds of T i , 1 <= i <= m. It is a dynamic type error if, in checked mode, V i is not a subtype of [V 1 , ..., V m /T 1 , ..., T m ]B i , 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 e f , then e f is evaluated to an object o f and f is bound to o f . Otherwise f is bound to null.

Observe that this is not in scope in e f . 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 (a 1 , …, a n , x n+1 : a n+1 , …, x n+k : a n+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 V 1 , ..., V m 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:

L et T i be the type parameters of R (if any) and let B i be the bounds of T i , 1 <= i <= m. In checked mode, it is a dynamic type error if V i is not a subtype of [V 1 , ..., V m /T 1 , ..., T m ]B i , 1 <= i <= m.

If q is a redirecting factory constructor of the form T(p 1 , …, p n+k ) = c; or of the form T.id(p 1 , …, p n+k ) = c; then the result of the evaluation of e is equivalent to evaluating the expression [V 1 , ..., V m /T 1 , ..., T m ]( new c (a 1 , …, a n , x n+1 : a n+1 , …, x n+k : a n+k )).

Otherwise, t he argument list (a 1 , …, a n , x n+1 : a n+1 , …, x n+k : a n+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 V 1 , ,.., V m 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(a

1

, .., a

n

)

or the form

new

T(a

1

, .., a

n

)

is

T.

It is a static warning if the static type of

a

i

, 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:

const type ('.' identifier )? arguments

;

Let

e

be a constant object expression of the form

const

T.id(a

1

, .., a

n

,

x

n+1

: a

n+1

, …, x

n+k

: a

n+k

)

or the form

const

T(a

1

, .., a

n

,

x

n+1

: a

n+1

, …, x

n+k

: a

n+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(a

1

, .., a

n

,

x

n+1

: a

n+1

, …, x

n+k

: a

n+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(a

1

, .., a

n

,

x

n+1

: a

n+1

, …, x

n+k

: a

n+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

a

i

, 1 < = i <= n + k,

is not a compile-time constant expression.

If

T

is a parameterized type

S<U

1

, ,.., U

m

>,

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 V

i

=

Dynamic

, otherwise let

V

i

= U

i

.

Evaluation of

e

proceeds as follows:

First, if

e

is of the form

const

T.id(a

1

, .., a

n

,

x

n+1

: a

n+1

, …, x

n+k

: a

n+k

)

then let

i

be the value of the expression

new

T.id(a

1

, .., a

n

,

x

n+1

: a

n+1

, …, x

n+k

: a

n+k

)

. Otherwise,

e

must be of the form

const

T(a

1

, .., a

n

,

x

n+1

: a

n+1

, …, x

n+k

: a

n+k

),

in which case let

i

be the result of evaluating

new

T(a

1

, .., a

n

,

x

n+1

: a

n+1

, …, x

n+k

: a

n+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 V i 1 <= i <= m, then:

For each instance variable f of i, let v if be the value of the f in i, and let v jf be the value of the field f in j. If identical( v if , v jf ) 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(a

1

, .., a

n

)

or the form

const

T(a

1

, .., a

n

)

is

T.

It is a static warning if the static type of

a

i

, 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

(a

1

, .., a

n

)

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

T

≪

S,

if one of the following conditions is met:

Reflexivity: T is S . T is bottom . S is Dynamic . Direct supertype: S is a direct supertype of T . T is a type variable and S is the upper bound of T. Covariance: T is of the form I<T 1 , ..., T n > and S is of the form I<S 1 , ..., S n > and T i ≪ S i , 1 <= i <= n. Transitivity: T ≪ U and U ≪ S .

≪ is a

partial order

on types.

T

is a

subtype

of

S

, written

T

<:

S,

iff

[bottom/

Dynamic

]T

≪

S

.

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.