Chapter 5. Types

Up to this point, we have not discussed types in any detail. We have been able to delay the discussion until now because Dart is optionally typed. In this chapter, we shall describe Dart’s types, filling in many gaps in the story we have presented so far. We begin by explaining optional typing, perhaps Dart’s most unusual characteristic.

5.1 Optional Typing

As noted in the introduction, a language is optionally typed if, and only if:

• Types are syntactically optional.

• Types have no effect on runtime semantics.

The latter point is far more significant than the former. Those accustomed to traditional statically typed languages may find this disconcerting at first. The point is subtle but crucial. It is a cornerstone of the Dart design.

First, it is essential that Dart can be used as a dynamically typed language; any program that can be written in such a language should be expressible in Dart. We expect code to evolve, gaining type annotations over time. If type annotations were to change the behavior of a Dart program, it is very likely that working programs might cease to function as expected as type annotations were added to them. This would discourage the use of type annotations, since programmers would fear that working code would malfunction.

Moreover, Dart programs will often include parts that are typed and parts that are not. This implies that one cannot assume type safety, and one cannot assume that a type annotation is in fact true. In such a situation, allowing type annotations to assume semantic significance could be confusing and destabilizing.

As an example of a language feature whose runtime behavior depends on types, consider type-based method overloading. Overloading is a common feature of statically typed object-oriented languages. In such a language, one might write

class NotLegalDart {
  overloaded(List l)=> l.length;
  overloaded(num n) => n*2;
}

and expect calls to resolve based on the type of the argument

int x = 3;
new NotLegalDart().overloaded(x); // presumably 6
List l = [];
new NotLegalDart().overloaded(l); // presumably 0

Now, what are we to make of

var x = 3;
new NotLegalDart().overloaded(x); // ?
var l = [];
new NotLegalDart().overloaded(l); // ??

On what basis are we to chose an implementation of overloaded? Presumably, this would be an error, forcing the user to specify types contrary to the notion of optional typing.1

1. Some might argue that we should consider the dynamic type of the argument in choosing the method. Such a mechanism, known as multi-methods has its own issues that are beyond the scope of this discussion.

Imagine further that we added a “catch-all” variant of overloaded

class NotLegalDart {
  ... as before
  overloaded(x) => x;
}

In this case, our second set of calls would resolve to the catch-all version, but if we decided to annotate the variables with types, the behavior would change just because we decided to document code at the call site.

Type based overloading is a problematic feature even in the presence of full static typing. Because types do not effect the semantics of Dart, the language cannot support type-based overloading. Consequently, all the examples we have shown will always behave the same in production, even if we decorate them with type annotations.

As another example consider

class LazyFields{
   var _x;
   get x => _x == null? _x = complicatedFunction() : _x;
}

Here we have a fairly common scenario where an instance variable is lazily initialized. Now suppose we decide to specify the type of _x:

class LazyFields{
   int _x;
   get x => _x == null? _x = complicatedFunction() : _x;
}

In many languages, the type of a variable influences its default value. It is typical to initialize an integer variable to 0. Unfortunately, the code will stop working in that case. Because of such issues, Dart adheres to the principle that type annotations have no semantic effect.

5.2 A Tour of Types

Dart variables may have types associated with them. Types may also be used to indicate the return types of methods. For example

int sum(int a, int b) => a + b;
void main(){ print(sum(3,4));} // prints 7

We can of course write code without types, as we have seen repeatedly in the preceding chapters. Here is very similar code, differing only in the absence of type annotations:

sum(a, b) => a + b;
main(){ print(sum(3,4));} // prints 7

These two variants behave exactly the same. The human reader benefits from the documentation provided by the type annotations, but the runtime doesn’t care.

Development tools, such as interactive development environments (IDEs) can take advantage of type annotations in various ways: they can issue warnings about possible inconsistencies, they can help the programmer by showing menus of methods that are potentially applicable to expressions (name completion) or by supporting automated code refactoring based on type information etc.

The simple example above contains no errors, so it is easy to see that the behavior of the two variants is the same. Suppose we change things a little

var i;
var j = 0;
sum(a, b) => a + b;
main(){ print(sum(i, j));} // NoSuchMethodError

Since i is not initialized, its value is null. When we execute sum, we end up calling the + method on null. Since + is not defined by Null, which is the class of null, we get a NoSuchMethodError. In some languages, you might get a null pointer error, but we know that in Dart, null, like all values, is an object and so it does respond to methods such as == etc.

The typed version below behaves identically at runtime. As discussed above, the fact that i is marked as an int does not change the way it is initialized.

int i;
int j = 0;
int sum(int a, int b) => a + b;
main(){ print(sum(i, j));} // NoSuchMethodError

The code fails, even though it is type correct. This is of course very common. If one could reliably ensure the correctness of realistic programs by type checking alone, truly statically typed programming languages would have an enormous economic advantage and would in fact be a “silver bullet” for all software bugs.

Our latest variant does contain errors, but it is type safe. Let’s take a version that isn’t type safe:

int i;
int j = 0;
Object sum(Object a, Object b) => a + b; // type warning: a has no method '+'
main(){ print(sum(3, 4));} // prints 7

This version works just fine, like our first two variants. However, the type warning is completely justified. According to the type annotations, an invocation like

sum(new Object(), []); // NoSuchMethodError

is supposed to make sense, but it clearly does not. The type annotations on sum are wrong, but the logic in the actual program is correct. Since type annotations cannot influence semantics, it follows that one can have incorrect annotations and still have a valid running program.

The type annotation on sum() is not quite as absurd as it seems.

sum('ab', 'surd'), // evaluates to 'absurd'

It turns out that sum() is a useful function on any number of types. Anything that has defined a + method makes sense. It might seem like marking the arguments with type Object is appropriate, as Object is the common super type of all these different types. As we have seen, this is not the case.

The best course of action is this case is to avoid type annotations on sum() altogether. This avoids egregious type warnings. If no type is explicitly given to a variable in the program, the variable’s type is dynamic. The type dynamic is a special type that denotes the fact that the type checker should not complain about operations on the variable or about assignments to/from the variable.

In contrast, explicitly using the type Object means we really expect that any object is a valid value for the variable. The two cases may appear similar, but when we operate on an expression of type Object, we will get warnings if we try to use methods that are not supported by all objects. In contrast, using the type dynamic effectively silences static type checking entirely. It tells the typechecker that we believe we know what we’re doing. This is useful in many situations where an accurate type is difficult (or impossible) to express.

In principle, one can write dynamic as a type annotation explicitly.

dynamic sum(dynamic a, dynamic b) => a + b; // Never do this!

However, this is pointless and very bad style. It conveys no information to either type checker or human. The absence of a type annotation means exactly the same thing, but without gratuitous clutter.

sum(a, b) => a + b; // So much better

To get a better sense of types and their use, let us return to our old friend Point:

class Point {
  num x, y;
  Point(this.x, this.y);
  Point scale(num factor) => new Point(x*factor, y*factor);
  Point operator +(Point p) => new Point(x + p.x, y + p.y);
  static num distance(Point p1, Point p2) {
   num dx = p1.x - p2.x;
   num dy = p1.y - p2.y;
   return sqrt(dx*dx + dy*dy);
  }
}

This version is fully annotated with types. Notice that we marked the instance variables with type num rather than int or even double. We chose num because it is the common supertype of integers and doubles, and points work well with coordinates of either type (or even a mixture of the two).

You can also see that the shorthand constructor using initializing formals does not include type annotations. The types of the parameters are derived from the instance variable declarations, so there is no need to repeat them.

5.3 Interface Types

Dart types are interface types. They define a set of methods available on an object. As a general rule, they do not tell us anything about the object’s implementation. Once again, this is in keeping with the basic principle that it is the object’s behavior that matters, not its pedigree.

Perhaps surprisingly, Dart has no syntax for interface declarations. Interfaces are introduced via class declarations. Each class introduces an implicit interface based on the signatures of its members. Cases that call for a traditional interface declaration are easily handled by defining a purely abstract class.

Any class can implement an interface, even if it has no relation to the class that defines the interface. This is the reason that interface declarations are unnecessary in Dart. We saw an example of such a class, Pair in Section 2.7.

abstract class Pair {
 get first;
 get second;
}

We can declare implementations of the interface defined by Pair

class ArrayPair implements Pair {
 var _rep;
 ArrayPair(a, b) {
  _rep = [a, b];
 }
 get first => _rep[0];
 get second => _rep[1];
}

The class ArrayPair implements Pair rather than subclassing it. The implements clause is followed by the names of one or more interfaces that the class is intended to implement.

A class does not inherit any implementation from the interfaces named in the implements clause. In this particular example, there is no implementation to inherit, but in many cases the distinction matters.

What the implements clause does is induce an explicit subtype relationship between the class and the interfaces the clause lists. The subtype relation affects the behavior of the Dart typechecker as well as the runtime.

To see the effects of the subtyping relations induced by the implements clause, let’s write a function that acts on pairs:

Pair reversePair(Pair p) => new ArrayPair(p.second, p.first);

We can use reversePair() as follows

reversePair(new ArrayPair(3, 4)); // a new Pair, with first = 4 and second = 3

The Dart type checker is perfectly happy with this code. However, if we had omitted the implements clause, the type checker would not know that ArrayPair is a subtype of Pair. In that case, it would complain both about the definition of reversePair() and about its use. At the definition of reversePair(), it would warn us that the returned object, of type ArrayPair was not a subtype of the declared return type Pair. A similar notification would be issued at the call site because the actual parameter has type ArrayPair whereas the formal parameter is of type Pair.

The type checker will perform such checks at every point where an object is transferred from one variable to another. These value transfers occur when:

• An assignment is performed.

• An actual argument is passed to a function.

• A result is returned from a function.

Each of the interfaces given in the implements clause is considered a direct super-interface of the class. In our example, Pair is a direct superinterface of ArrayPair. In addition, the immediate superclass of a class is also considered to be one of the classes’ direct superinterfaces.

The complete set of superinterfaces of a type can be computed by taking the set of direct superinterfaces, and recursively computing and adding the superinterfaces of each. The computation is complete when no new element can be added to the set.

Strictly speaking, the typechecker does not enforce the superinterface relation when checking value transfers. What is checked is assignability. Assignability is more liberal than subtyping. Dart considers types assignable if they are subtypes in either direction. That is, not only is ArrayPair assignable to Pair (because ArrayPair is a subtype of Pair) but Pair is assignable to ArrayPair. The latter rule may well puzzle you. After all, there could be many implementations of Pair and they would usually not have all the members of ArrayPair. To understand why Dart acts this way, let’s look at another example.

class Monster {
  // many monstrous features
}

class Vampire extends Monster {
  get bloodType => 'O';
}

Map<String, Monster> monsters = {
 'Frankenstein' : new Monster(),
 'Godzilla' : new Monster(),
 'Dracula' : new Vampire()
}
...
Vampire vamp = monsters['Dracula'];

The monster map monsters is only guaranteed to contain instances of Monster. A typical type checker would complain that the result of the lookup on the last line above is a Monster, which is not a subtype of Vampire.

In practice, when we look up the key ‘Dracula’ in the monster map monsters we know that we’re going to get a Vampire back. Such situations arise frequently. Using conventional type checking rules, one would be forced to use casts. Imposing such a burden upon programmers runs counter to Dart’s philosophy of using types as a tool to improve the developer experience. Instead, Dart’s assignability rules support implicit downcasting.

Of course, this means that Dart’s type discipline is unsound. We cannot guarantee that a Dart program that passes the type checker is in fact free of runtime errors. However, as we noted in the tour above, in reality no static type system can do so. Typically, those properties that a type system can enforce are defined to be part of its purview, while those it cannot are relegated elsewhere. A classic example is pattern matching in functional languages, which defines the enforcement of certain properties to lie outside the static type system.

As we shall see in Section 5.5, there are other reasons why Dart type checking is not sound.

5.4 Types in Action: The Expression Problem, Typed

To get a better sense of types in Dart, let us revisit the expression problem, which we first encountered in Section 2.15.1. We’d like to type check our original solution.

We’ll start by adding type information to our evaluator. When typechecking the evaluator we need to know that the mixins for addition and subtraction have fields that support an int valued eval() method, so we annotate the fields with the type ExpressionWithEval. We still have no dependency on other libraries (except dart:core of course). We don’t need to know whether ExpressionWithEval is actually a subtype of Expression for example.

We also want to guarantee that the mixin for numbers has a field val of type int, and that all eval methods return integers.

library evaluator;

abstract class ExpressionWithEval {
 int get eval;
}

abstract class AdditionWithEval {
 ExpressionWithEval get operand1;
 ExpressionWithEval get operand2;
 int get eval => operand1.eval + operand2.eval;
}

abstract class SubtractionWithEval {
 ExpressionWithEval get operand1;
 ExpressionWithEval get operand2;
 int get eval => operand1.eval - operand2.eval;
}

abstract class NumberWithEval {
 int get val;
 int get eval => val;
}

However, when adding types to multiplication evaluator we find that we need to add an import, because we must access the type ExpressionWithEval. It is not surprising that evaluation code for all types might depend on the type defined in the original evaluator, but it is just a tad regrettable that we can no longer compile these independently.

library multiplication evaluator;
import 'evaluator.dart' show ExpressionWithEval;

abstract class MultiplicationWithEval {
 ExpressionWithEval get operand1;
 ExpressionWithEval get operand2;
 int get eval => operand1.eval * operand2.eval;
}

The code for the string converter library is annotated much like that of evaluator:

library string converter;

abstract class ExpressionWithStringConversion {
  String toString();
}

abstract class AdditionWithStringConversion {
   ExpressionWithStringConversion get operand1;
   ExpressionWithStringConversion get operand2;
   String toString() => '$operand1 + $operand2';
}


abstract class SubtractionWithStringConversion {
  ExpressionWithStringConversion get operand1;
  ExpressionWithStringConversion get operand2;
  String toString() => '$operand1 - $operand2';
}

abstract class NumberWithStringConversion {
  int get val;
  String toString() => '$val';
}

abstract class MultiplicationWithStringConversion {
  ExpressionWithStringConversion get operand1;
  ExpressionWithStringConversion get operand2;
  String toString() => '$operand1 * $operand2';
}

Strictly speaking, since all objects support toString(), we need not have annotated our operands with type ExpressionWithStringConversion or even defined ExpressionWithStringConversion. All that was really required was to note that our toString() methods returned String so we could check that the values being returned from these methods were indeed strings. However, a key goal of types in Dart is clarity, and we want to emphasize that we are using the same pattern as in the evaluator here as well.

The fact that libraries that introduce new functions to the expression hierarchy do not depend on the core class hierarchy also means that one could annotate these libraries with types regardless of whether we chose to add type annotations to the core class hierarchy. Hence, programmers who are fond of type checking can add new functions to the system and type check them even if the designers of the original hierarchy chose not to use types at all.

Now let us annotate the types of fields in our class hierarchy. Doing so requires us to import type Expression.

library abstract expressions;
import 'expressions.dart' show Expression;


abstract class AbstractExpression{}


abstract class AbstractAddition {
 Expression operand1, operand2;
 AbstractAddition(this.operand1, this.operand2);
}

abstract class AbstractSubtraction {
 Expression operand1, operand2;
 AbstractSubtraction(this.operand1, this.operand2);
}

abstract class AbstractNumber {
 int val;
 AbstractNumber(this.val);
}

library multiplication;
import 'expressions.dart' show Expression;

abstract class AbstractMultiplication {
 Expression operand1, operand2;
 AbstractMultiplication(this.operand1, this.operand2);
}

The dependency on the expressions library is more unpleasant than the dependency of multiplication evaluator on evaluator. We had originally defined our core hierarchy completely independently of the final application library. It would seem that adding types is making our code less modular. We shall see how to obtain the desired decoupling while keeping the types when we discuss generics in the next section.

Finally, let’s add types to our main library. The only change we need to make is annotate the local variable e with the type Expression. The rest of the library is unchanged.

main(){
 Expression e = new Multiplication(
                      new Addition(new Number(4), new Number(2)),
                        new Subtraction(new Number(10), new Number(7))
                );
 print('$e = ${e.eval}'),
}

Altogether it has been straightforward to add types to our expressions code. The only real issue is that all uses of Expression and its subclasses have to be typechecked against the types defined by the mixin applications. To overcome this issue, we’ll need more sophisticated types: generics.

5.5 Generics

Dart classes may be generic—that is, they may be parameterized by types.

Generic classes can be given actual type parameters:

List<String> l;
Map<String, int> m;

Types like these are called parameterized types.

Providing type parameters to a generic class is not required however. If one chooses to use the name of a generic class without type arguments, the type dynamic is used implicitly in place of all missing type arguments. So one can write

List l;
Map m;

and this would be fully equivalent to

List<dynamic> l;
Map<dynamic, dynamic> m;

Obviously, the former, more concise form is preferred. It is bad style to clutter the code with types that convey no new information. There are, however, situations where writing the type dynamic explicitly is desirable, as in

Map<String, dynamic> m;

If one provides the wrong number of type arguments, all type arguments are disregarded.

Map<String> m; // Map
List<String, num> // List
Object<int> // Object

The above three examples will each result in a static warning. Aside from the warning, the above are treated as Map, List and Object respectively, which are, as noted above, equivalent to Map<dynamic, dynamic>, List<dynamic> and Object.

Suppose we have

class Fruit {
  var color;
  Fruit(this.color);
}


class Apple extends Fruit {
  Apple():super('Red'),
}


class Orange extends Fruit {
  Orange():super('Orange'),
}

// print colors of all fruits in a list
printColors(List<Fruit> fruits) {
  for (Fruit f in fruits) print(f.color);
}


main() {
 List<Apple> apples = <Apple>[];
 List<Orange> oranges = new List<Orange>();
 apples.add(new Apple());
 oranges..add(new Orange())
        ..add(new Orange());
 printColors(apples); // prints Red once
 printColors(oranges); // prints Orange twice
}

We have two classes, Apple and Orange both of which are subclasses of Fruit. We have a simple function, printColors() that takes lists of Fruit and prints the color of each member in the list. We can use this function with apples or oranges because both are lists of Fruit. This is intuitive—if Apple is a Fruit, surely a List<Apple> is a List<Fruit>?

If you’ve ever delved into the strange world of generic types, you realize that life can be a lot more complicated. Nevertheless, in Dart, it is indeed the case that a List<Apple> is a List<Fruit>.

More generally, if G is a generic class with n type parameters, then if Si is a subtype of Ti for i 2 1..n, then G < S1, . . . , Sn ><: G < T1, . . . , Tn >, where T <: S indicates that T is a subtype of S.

This behavior is known as covariance.

It is well known that covariant subtyping of generics is problematic.

addApple(List<Fruit> fruits) {
  fruits[fruits.length-1] = new Apple();
}
 addApple(oranges); // now oranges contains an Apple!

The sad fact is that the intuitive relation most people expect does not actually hold in an imperative language. This confronts language designers with an unpalatable choice: produce an unsound type system, which means that you cannot rely on it; or go with the rules of logic, leaving most programmers utterly baffled. The author has been through this exercise several times. In Dart, we have deliberately chosen to make the type system unsound.

5.5.1 The Expression Problem with Generics

In our previous iteration over the expression problem (5.4), we found that adding types introduced an undesirable dependency into our class hierarchy libraries; they became dependent upon the leaf type Expression. We shall now see how generics can be used to alleviate this problem.

Our first step is to make the core classes generic, parameterized by E. E represents the final expression type in our application. This gets rid of the dependency on Expression.

library abstract expressions;

abstract class AbstractExpression{}

abstract class AbstractAddition<E> {
 E operand1, operand2;
 AbstractAddition(this.operand1, this.operand2);
}

abstract class AbstractSubtraction<E> {
 E operand1, operand2;
 AbstractSubtraction(this.operand1, this.operand2);
}

abstract class AbstractNumber {
 int val;
 AbstractNumber(this.val);
}

library multiplication;

abstract class AbstractMultiplication<E> {
 E operand1, operand2;
 AbstractMultiplication(this.operand1, this.operand2);
}

We’ll also parameterize the mixin classes that represent our functions. In the evaluator mixins, we need a type parameter that is known to be a subtype of Expression-WithEval so that we can invoke the eval method without getting warnings.

library evaluator;


abstract class ExpressionWithEval {
 int get eval;
 }

abstract class AdditionWithEval<E extends ExpressionWithEval> {
 E get operand1;
 E get operand2;
 int get eval => operand1.eval + operand2.eval;
}

abstract class SubtractionWithEval<E extends ExpressionWithEval> {
 E get operand1;
 E get operand2;
 int get eval => operand1.eval - operand2.eval;
}

abstract class NumberWithEval {
 int get val;
 int get eval => val;
}

library multiplication evaluator;

import 'evaluator.dart' show ExpressionWithEval;

abstract class MultiplicationWithEval<E extends ExpressionWithEval> {
 E get operand1;
 E get operand2;
 int get eval => operand1.eval * operand2.eval;
}

Similarly, the mixins for string conversion should have E bounded by Expression-WithStringConversion:

library string converter;


abstract class ExpressionWithStringConversion {
  String toString();
}


abstract class AdditionWithStringConversion<E extends ExpressionWithStringConversion> {
  E get operand1;
  E get operand2;
  String toString() => '$operand1 + $operand2';
}


abstract class SubtractionWithStringConversion<E extends ExpressionWithStringConversion> {
  E get operand1;
  E get operand2;
  String toString() => '$operand1 - $operand2';
}

abstract class NumberWithStringConversion {
  int get val;
  String toString() => '$val';

abstract class MultiplicationWithStringConversion<E extends ExpressionWithStringConversion> {
  E get operand1;
  E get operand2;
  String toString() => '$operand1 * $operand2';
}

Finally, we must modify expressions to instantiate all these parameterized types.

library expressions;


import 'abstractExpressions.dart' show
      AbstractAddition,
      AbstractExpression,
      AbstractNumber,
      AbstractMultiplication,
      AbstractSubtraction;


import 'evaluator.dart' show
      AdditionWithEval,
      ExpressionWithEval,
      MultiplicationWithEval,
      NumberWithEval,
      SubtractionWithEval;


import 'multiplication.dart';

import 'multiplicationEvaluator.dart';

import 'stringConverter.dart' show
      AdditionWithStringConversion,
      ExpressionWithStringConversion,
      MultiplicationWithStringConversion,
      NumberWithStringConversion,
      SubtractionWithStringConversion;


abstract class Expression = AbstractExpression with
              ExpressionWithEval, ExpressionWithStringConversion;


class Addition = AbstractAddition<Expression> with
      AdditionWithEval<Expression>,
      AdditionWithStringConversion<Expression>
      implements Expression;


class Subtraction = AbstractSubtraction<Expression> with
      SubtractionWithEval<Expression>,
      SubtractionWithStringConversion<Expression>
      implements Expression;


class Number = AbstractNumber with
              NumberWithEval, NumberWithStringConversion
              implements Expression;


class Multiplication =
  AbstractMultiplication<Expression> with
            MultiplicationWithEval<Expression>,
            MultiplicationWithStringConversion<Expression>
            implements Expression;

This concludes our discussion of the expression problem. Remember that the issue of extending a set of type variants and functions upon them is universal, and the patterns shown here can be helpful for many applications that have nothing to do with expressions. The referenced papers by Torgersen[8] and by Zenger and Odersky[9] are highly recommended for those interested in diving deeper.

5.6 Function Types

Function types are treated specially in Dart. All functions implement Function, but function types are compared based on their structure.

The type of a function is based on its return type and on the types of its formal parameters. The type reflects whether the parameters are named or positional, whether the positional parameters are required, and the names of the named parameters.

At this writing, Dart has no syntax for expressing function types, so we will introduce a special notation here. In the simplest case, a function takes n positional parameters, all of whom are required. As a special case, n can be 0. Assuming the formal parameters have types T1, . . . , Tn and the return type is T, we can write the function type as Ft = (T1, . . . , Tn) → T. For example, we would write the type of

 num twice(num x) => x * 2;

as (num) → num, and the type of

num product(List<num> nums) => nums.reduce( (a, b) => a * b);

is (List < num >) → num. The type of reduce() itself is interesting, because reduce() is a function that takes a function as its argument. The type of reduce() is ((E, E) → E) ! E, where E is the type parameter of List. In our example, nums is a List<num> and so num is substituted for E, so that List<num>.reduce() has the type ((num, num) ! num) → num.

However, the type of the actual function argument passed to reduce() is (dynamic, dynamic) → dynamic.

This is a perfectly valid argument, because (dynamic, dynamic) → dynamic is a subtype of (num, num) → num. This makes sense, because the function type accepts arguments of type dynamic, which include type num. Its result type is dynamic, which is acceptable everywhere.

Suppose we wrote product slightly differently

num product(List<num> nums) => nums.reduce( (num a, num b) => a * b);

Now the argument to reduce() has type (num, num) → dynamic, which is still perfectly valid. Suppose we change the code ever so slightly to

num product(List<num> nums) => nums.reduce( (int a, int b) => a * b);

The argument of reduce() now has type (int, int) → dynamic. So is (int, int) ! dynamic a subtype of (num, num) → dynamic? You might be forgiven for thinking so—after all, int is a subtype of num. Alas, if we were to use this version with a list of numbers that included doubles, reduce() would call its argument function with a double extracted from the list; but doubles are not integers, and double is not assignable to int.

We can now ask what are the general rules for subtyping among functions? Given Ft and another function type Fs = (S1, . . . , Sn) → S, when can we assume Ft is a subtype of Fs?

Type theory tells us that this only holds if T is a subtype of S and if, for i 2 1..n, Si is a subtype of Ti. That is, the type of each formal parameter of Fs must be a subtype of the corresponding formal in Ft. Notice that the direction of the subtyping relation is reversed. This phenomenon is known as contravariance. The logic is inescapable. Suppose we pass a function f of type Ft to a function h(g). If the formal parameter g has type Fs, it may be safely passed actual parameters that are subtypes of the Si. It follows that each of the formal parameters of f of type Ti must accept any subtype of Si, and so Ti must be a supertype of Si.

Unfortunately this reasoning, while unassailable, runs counter to the intuitions of almost everyone. Experience shows that reasoning about the types of higher order functions is difficult. Dart deliberately seeks to avoid confronting the programmer with such complex types as much as possible.

As we have already noted, type soundness is not a goal of Dart. So it seems natural to abandon it and conform to the common (if incorrect) intuition and adopt a covariant rule, which would state that Ft is a subtype of Fs if T is a subtype of S and Ti is subtype to Si for i 2 1..n. In fact, Dart’s rule is somewhat different.

Recall that the intuition behind the subtype rules for function types is based on what constitutes a valid invocation of a function valued parameter g to a higher-order function h. For ordinary (non-function) objects, parameter passing is governed by the assignability rule, which allows subtyping in either direction. Hence, when g is invoked with actual arguments, their types are only required to be assignable to the formal parameters of g. Any argument must have a type assignable to the appropriate Si, and so it must be assignable to Ti. In other words, the argument can be either a subtype of Ti or a supertype of Ti. Hence, it is acceptable if Ti is either a subtype or a subtype of Si. We therefore assume that Ft is a subtype of Fs if T is assignable to S and Ti is assignable to Si.

So far so good. The rule above, while unsound, is very liberal. Programmers who run afoul of it are indeed likely to be passing a function whose parameters have types completely unrelated to the formal parameter.

We still have to account for optional and named parameters.

5.6.1 Optional Positional Parameters

As a first step, let us extend our notation for function types to accommodate optional positional parameters. A function might have k optional positional parameters. We’ll write them after the required parameters, inside square brackets, echoing the syntax used in function signatures: (T1, . . . , Tn, [Tn+1, . . . , Tn+k]) → T.

We can safely pass a function that takes optional parameters to contexts where those parameters are required. The function can accept them. The converse situation is unacceptable. A function that requires certain parameters cannot be used in a context where they are deemed optional, since they may not in fact be provided. The subtype must be able to accept at least as many parameters as the supertype. However, not all of the subtype’s parameters need be required parameters; in fact, it’s fine if none of them are required.

This leads us to the actual rule:

A function type Ft = (T1, . . . , Tn, [Tn+1, . . . , Tn+k]) → T is a subtype of a function type Fs = (S1, . . . , Sj, [Sj+1, . . . , Sm]) → S iff:

1. j n and m n + k.

2.i Image 1..m, Ti is assignable to Si.

3. Either S is void or T is assignable to S.

The subtype requires n parameters, whereas the supertype has j required parameters. When a function ft of type Ft is used in a place that requires type Fs, it will always be passed at least j parameters. Since Ft requires n parameters, the first item insists that j n, so ft is guaranteed to get as many parameters as it requires. The maximum number of parameters that might be passed is m, the total number of parameters of Fs. The function ft will accept up to n + k parameters—n required and k optional. As long as m is no larger than n + k, the number of parameters passed will be acceptable to ft. The types of the actual arguments must be assignable to the formals of ft. Since at most m actuals will in fact be sent, we only require assignability for the first m parameter types.

The last item is a twist we’ve neglected so far. Dart allows you to identify functions that are not intended to return a result. These can be marked using the special return type void. If Fs returns void, the type checker assumes the result is not used, and so it has no significance.

5.6.2 Named Parameters

What of named parameters? Again, let’s introduce some notation for the types of functions with named parameters. We will describe named parameters between braces similar to Dart map literals, again echoing the concrete syntax used in function signatures. The named parameters appear after the positional ones. Each named parameter will be denoted by a type followed by its name.

As a concrete example we’ll look at the method firstWhere() of Iterable.

E firstWhere(bool test(E value), {E orElse()});

which has type ((E) → bool, {() → E orElse}) → E.

The firstWhere() method takes a predicate as an argument, just like where(). Instead of returning a collection of results for which the predicate holds, firstWhere() simply returns the first such element.

A close relative of firstWhere() is lastWhere() which returns the last element for which the predicate holds.

E lastWhere(bool test(E value), {E orElse()});

So the general form of a function type with named arguments is (T1, . . . , Tn, {Tx1x1, . . . , Txkxk}) → T Let us consider the subtyping rules for such function types. Since named parameters are always optional, a logic similar to what we used in the last subsection applies. We can pass a function that takes named parameters to a context that does not specify named parameters as long as its return type and required parameters are acceptable.

If the formal parameter does specify named parameters we must ensure that the incoming argument accepts all of the named parameters mentioned in the supertype; it can also accept additional ones. Notice that we make no mention of optional positionals here. In Dart, a function cannot have both optional positional parameters and named parameters.

We can state these rules formally as follows:

A function type Ft = (T1, . . . , Tn, {Tx1x1, . . . , Txkxk}) → T is a subtype of a function type Fs = (S1, . . . , Sn, {Sy1y1, . . . , Symym}) → S iff:

1. Either S is void or T is assignable to S.

2.i Image 1..n, Ti is assignable to Si.

3. km and yi Image {x1, . . . , xk}, i Image 1..m.

4. For all yi Image {y1, . . . , ym}, yi = xjTxj is assignable to Syi.

5.6.3 Call() Revisited

In Section 4.6.1 we mentioned that any class that declares the special method name call implicitly implements Function. We can now refine our understanding of the behavior of call.

Suppose we wanted to define a class of maps that also behave as functions from the keys of the map to its values. We’ll restrict this to maps from strings to numbers.

class MapFunction implements Map<String, num> {
  MapFunction(this._map);
  Map<String, num> _map;
  num operator [](String k) => _map[k];
  void operator []= (String k, num v) { _map[k] = v;}
  // more map wrapping functions
  num call(String key) => _map[key];
}

Instances of class MapFunction are created by passing a map object to the class’ constructor. The class then forwards all calls in the Map interface to the map object it wraps. In addition, the call method takes an object, uses it as a key into the underlying map and returns the result of the lookup.

Dart ensures not only that MapFunction is considered to implement Function, but that it is considered a subtype of Stringnum.

In general, when a class FunctionWannabe declares a call() method, the signature of that method describes a function type F. FunctionWannabe is considered to be a subtype of F. Above we showed this for F = Stringnum and F unctionW annabe = MapF unction.

5.7 Type Reification

While type annotations have no presence at runtime, other aspects of types do. As we have already seen, each object carries with it its runtime type which can be accessed dynamically via the runtimeType method inherited from Object. Users are free to override runtimeType which means that the implementation type of an object is not, in general, observable via calls to runtimeType.

Each type declaration introduces a compile-time constant object of class Type representing it. These objects are available at runtime as well. Moreover, one can test whether an object is a member of a type via dynamic type tests and type casts, as shown below.

5.7.1 Type Tests

Type tests are expressions that test whether an object belongs to a type

var v = [1, 2, 3];
v is List; // true
v is Map; // false
v is Object; // POINTLESS: always true

The general form of a type test is e is T where e is an expression and T is a type. The type test evaluates e and tests the dynamic type of the result against the type T. The last line in the above code snippet is something that should never appear in a normal Dart program. It will always evaluate to true, because all Dart values are objects.

Because a Dart class can implement an unrelated type, an object may emulate another type T even if the class of the object is not a subclass of T. However, an object cannot completely hide its implementation type, because one can detect it using a type test. This is arguably a violation of the principle that only the behavior of an object should matter. The same argument holds for casts, described below.

5.7.2 Type Casts

Type casts also evaluate an expression and test whether the resulting object belongs to a type, but they are not a predicate. Instead, the cast will throw a CastError if the test fails; otherwise it returns the object unchanged.

Object o = [3, 4, 5];
o as List; // a somewhat costly no-op
o as Map; // throws

A cast is pretty much a shorthand for

var t = e;
t is T ? t : throw new CastError();

A typical use of casts might be to validate data

List l = readNextVal() as List;
  // I'm pretty sure I'll get a list back from readNextVal()
  // if not, things are really hosed and I should fail
  // Next, do stuff with a list

One should be careful not to abuse casts. Consider

Object o = [5, 6, 7];
... // lots of intervening logic
o.length; // works; type warning: objects don't always have length

Many programmers may be tempted to rewrite the code into

Object o = [5, 6, 7];
... // same intervening logic
(o as List).length; // BAD! The wrong way to avoid a warning

Casts are executed at runtime and therefore come with runtime cost. If your goal is simply to silence the type checker, just use an assignment:

Object o = [5, 6, 7];
... // same intervening logic
List l = o;
l.length;

The above works well because of the assignability rule (5.3). Some programmers might object to this idiom because they find it difficult to keep coming up with names for variables. A simple solution is to append a variant of the cast syntax to the variable name:

Object o = [5, 6, 7];
... // same intervening logic
List o_as_List = o;
l.length;

5.7.3 Checked Mode

During development, it is often very useful to validate the types of variables. For example, we’d like to ensure that incoming parameters or an object returned from a call meet expectations. Dart provides checked mode for this purpose. In checked mode, every value transfer is dynamically checked. This means that a dynamic type test is performed automatically for you every time a parameter is passed, a result is returned from a method or function, and on every assignment executed. Checked mode ensures that the dynamic value being assigned to a variable is a member of the static type of the variable being assigned to. Likewise, the dynamic type of an actual parameter is tested against the formal parameter’s static type, and the dynamic type of a function’s result is tested against the declared return type of the function.

num n = 3.0;
int i = n; // dynamic error in checked mode; works in production
num x = i; // always works
int j = null; // always allowed

Checked mode’s behavior differs from the static type checking rules. When assignments are checked statically, we use the assignability rule (5.3) that permits the assignment if a subtype relation holds in either direction. The dynamic check implemented by checked mode insists that the actual type of the value being assigned is either a subtype of the static type of the variable, or null.

We can see this difference in the assignment to i above. The assignment will not cause a static type warning, but does fail in checked mode.

In the absence of checked mode, one could insert typecasts into code, but that would be undesirable. The casts are not only tedious; they introduce runtime overhead, so their systemic use is prohibitively expensive in production. Type casts should be used only when truly necessary.

As you can see, in checked mode, type annotations will impact program behavior; however, this is a developer tool explicitly under programmer control for the purpose of verifying the correctness of the type annotations. Indeed, in checked mode, type annotations act like assertions. Checked mode also activates any assert statements in the program (6.2.7).

5.7.4 Reified Generics

Type arguments are reified at runtime. When a generic class is instantiated, the actual type arguments provided are in fact passed at runtime and stored. Thus, the class of an instance created with new List<String>() is in fact different from the class of an instance created via new List<Object>().

We can write

var l = new List<String>();
l is List<String>; // true
l is List<int>; // false

One must understand the limitations of such tests in Dart however. Because the type system is unsound there is no hard guarantee that an object that claims to be, say, a List<int> contains only integers. We can be confident that the object is a list, and that it was created as a list of integers. However, any kind of object could subsequently have been inserted into it.

In checked mode however, we have a lot more confidence. Any attempt to corrupt the list by inserting an inappropriate object into it would be trapped in checked mode. Checked mode tests the actual type of objects against the declared types of variables and function results; but in a generic type, the actual type arguments to the generic are substituted for any type variables in the declared types. So, given that List defines

operator [int index]= (E e)

when one writes

var l = new List<String>();
l[0] = 'abc'; // always ok
l[1] = 42; // fails in checked mode - 42 is an int which is not a subtype of String

checked mode will ensure that uses of an instance of a generic class cannot be corrupted.

5.7.5 Reification and Optional Typing

How does the concept of optional typing fit in with type reification? In particular, optional types are not supposed to influence runtime semantics, and yet reification clearly does. However, reification only impacts behavior when the program explicitly attempts to observe or determine runtime type structure. These situations are:

• Asking an object for its type, using a type test, cast or a call to runtimeType.

• Setting the runtime type of an object by passing actual type parameters to the constructor of a generic type.

• Using reflection to check or set the type of a variable or function.

• Using checked mode.

• Determining the reified type of a function object via annotations on its signature.

The final three points all have the potential to be impacted by type annotations, which normally have no semantic effect. Of these, the last point is perhaps the most subtle one. If logic depends on a type test involving a function, it can be influenced by a type annotation. The function could be a closure defined explicitly or extracted as a property. The following code demonstrates the issue:

typedef int IntFunction(int);

observeAnnotations(f) {
   return f is IntFunction;
}

String id1(String x) => x;
id2(x) => x;
int id3(int x) => x;

observeAnnotations(id1); // false
observeAnnotations(id2); // true
observeAnnotations(id3); // true

All three functions id1, id2, id3 are the same except for their type annotations, but they differ in their behavior with respect to type tests due to the type annotations used. Of course, we are already performing a type test explicitly, so it is not that surprising that types should have an effect on behavior in this case.

5.7.6 Types and Proxies

Being able to define transparent proxies for any sort of object is an important property. There is an inherent tension between proxying and typing. Consider our general purpose proxy class Proxy introduced in Section 2.10.

Assume the proxy is emulating type Point. The first difficulty we run into is when we use an instance of Proxy as the value of a typed variable or parameter.

Point pointProxy = new Proxy(new Point(0,0)); // warning

Since Proxy is neither a subtype nor supertype of Point, the code causes a warning. The best solution is to declare proxyPoint as a dynamically typed variable.

var pointProxy = new Proxy(new Point(0,0));

Of course, at this point we have lost certain benefits of type checking. If we want to type check our use of pointProxy we’ll need another approach. It might seem that all we need to do is add

Point pointImposter = pointProxy;

but this turns out to be insufficient. While this does silence the static warning, we will run into trouble with dynamic type checks. If we were to write any of the following

pointImposter is Point; // false!
pointImposter as Point; // exception!

our pretense that our proxy object is a point will be exposed. While we might choose to forego type checking and avoid these situation, in reality we may have to pass pointProxy or pointImposter to code written by others that assumes it works on points. In that case it is quite likely that dynamic tests like the above will occur. Moreover, when running in checked mode, failures will occur the moment one tries to perform assign, pass or return pointProxy to a context that is annotated with type Point.

Point pointProxy = new Proxy(new Point(0,0)); // dies in checked mode

If we are to engage in any kind of type checking, we need to define a type-specific proxy:

class PointProxy extends Proxy implements Point {
 PointProxy(forwardee): super(forwardee);
}

Now we can use our proxies reliably:

Point pointProxy = new PointProxy(new Point(0,0));
pointProxy is Point; // true!
pointProxy as Point; // identical to pointProxy

But wait! There are no methods for x, y, rho, theta, + and so on in PointProxy. Won’t we get warnings that PointProxy doesn’t implement the Point interface correctly? As it happens, Dart has a special rule that lets us suppress such warnings. If a class declares a noSuchMethod() then no complaints about missing members of support interfaces are given, nor are any such warnings given about incorrect signatures of such members.

The need for type-specific proxies is disappointing. Here, most of the work is done by Proxy but we still have to define per-interface subclasses. This solution is much easier and more maintainable than defining an exhaustive set of forwarding methods, which is what you would have to in a traditionally typed setting, but not as easy as just defining a single Proxy class. In a fully dynamic setting, the shared definition of Proxy would suffice.

We considered several alternatives. Having type tests (including those in checked mode) and casts have user-definable behavior would help, but would entail a performance overhead that the design team was not willing to pay.

A related issue is emulating functions, first introduced in Section 4.6.1. We cannot use the exact same technique as above however, since we cannot implement a specific function type. However, there is an alternative.

The problem arises in practice in Dart’s unit test framework. The framework defines a method expectAsync() which takes an asynchronous function and wraps it in a proxy so it appears to be synchronous. The framework needs to work with target functions of various types and arities.

We want the function proxy to have the same signature as its target to avoid any difficulties with dynamic type tests. If we knew the precise function signature we sought to emulate, we could define a suitable call method, which would automatically make our proxy class a member of the corresponding function type.

In addition, we need to be able to forward calls from the proxy to its target. Again, this needs to work for varying arities.

A pragmatic solution relies on the fact that we can realistically limit the arity of the functions involved to some fixed value n. Assume n = 5 for the sake of our examples, but if that is insufficient one can choose n = 10 or n = 255 or whatever value suits—the principle is the same. We will also ignore the possibility of using named arguments. In this case, we can define call to take five optional parameters. This makes our proxy a member of the type ([dynamic, dynamic, dynamic, dynamic, dynamic]) ! dynamic which is a subtype of any function with no more than five required arguments and no named arguments. As a result, the proxy object not only supports the same legal calls as its target, but operates safely even with respect to type tests, casts and in checked mode.

It remains to define the code in our call method to forward calls to the target. Our implementation needs to determine the proper number of arguments to pass along. Assuming the call to the proxy function has the correct arity, we need to determine how many arguments were in fact passed to us, and then call the target with those.

To accomplish this, we set the default value for each argument to a special marker object. Since default parameter values must be constant in Dart, the marker object is a compile-time constant object. We can then define the argument list conveniently as a list literal with all formal parameters, filtering out all occurrences of our marker value using the removeWhere() method on lists. We can then use Function.apply(), which we first saw in Section 4.6, to forward the call. Here is the necessary code

class UnsentArgumentMarker{
  const UnsentArgumentMarker();
}
const NO ARG = const UnsentArgumentMarker();
class ProxyForFunctionOfArity5 implements Function {
  Function target;
  ProxyForFunctionOfArity5(this.target);
  call([a0 = NO ARG, a1 = NO ARG, a2 = NO ARG, a3 = NO ARG, a4 = NO ARG]){
    List args = [a0, a1, a2, a3, a4]..removeWhere((arg) => arg == NO ARG);
    return Function.apply(target, args);
  }
}

The real code in the unit test framework has some extra logic to deal with handling asynchrony, but that is not our concern here. We could filter out the unsent arguments in other ways, but this code is elegant and concise at some slight expense in efficiency. What is important to note here is how we have emulated a broad set of function types and the technique of determining which arguments were actually passed along.

Dart’s type system makes one more special dispensation for proxies. The annotation (7.3) @proxy has special significance to the type system. If a class is annotated with @proxy, then the Dart analyzer will refrain from issuing warnings when a method is accessed on an instance of the class, even if the class is not statically known to support such a method.

5.8 Malformed Types

What happens if we annotate a variable with a type that does not exist? According to the principles we’ve elucidated above, the code should function unchanged except in checked mode.

UndefinedType v = 91;

Assuming UndefinedType is in fact undefined, Dart will issue a warning. In checked mode, the assignment to v will cause a runtime error when the system tries to check that int is a subtype of UndefinedType. However

UndefinedType v = null;

won’t cause any errors at runtime, because no subtype test is performed when the value being assigned to a variable is null.

Undefined types may arise due to varied causes: typographical errors, forgotten imports or as a result of mentioning a type before actually defining it. In some cases, the name used in a type annotation might denote a function or variable. In any case where the name does not denote an actual type declaration, we have a malformed type.

Undefined types are a particular form of malformed types. Ambiguous types—types whose meaning is not well specified because different declarations of the same name were imported into the same scope—are also malformed.

Genericity may give rise to malformed types as well. If the generic itself is malformed, any invocation of the generic is malformed as well:

UndefinedGeneric<int> ugi; // malformed
UndefinedGeneric ug; // malformed

Finally, type variables are considered malformed when they are referred to in the declaration of a static member:

class C<T, S> {
  static T cantReferToTypeVars; // warning
  static S alsoCantReferToTypeVars(T t) { // two warnings here
    S local; // another here
    return new Map[T, S](); //and two here
    }
}

Type variables have potentially distinct values for each instance of a generic class. They have no meaning when no instance is involved. It would be possible to simply consider type variables out of scope in static member declarations, but by treating them as in-scope but malformed, Dart can provide better error messages.

The declaration of cantReferToTypeVars gives a warning because T is malformed; it only has meaning in the context of an instance of C. The method alsoCantReferToTypeVars will yield multiple warnings: its return type S is malformed, as is the type of its formal parameter. The local variable local also gives rise to a warning, and the instantiation of Map in the final statement of the method yields separate warnings for S and T.

Once a warning has been issued about an occurrence of a malformed type, the static type checker replaces the occurrence with dynamic to prevent a pointless cascade of secondary warnings. For example, if we call alsoCantReferToTypeVars, we won’t get any warnings:

int i = C.alsoCantReferToTypeVars('abc'),

There is no point in bombarding the programmer with warnings that are not actionable. One could not make the call correct by changing the type of the variable i or the type of the argument. The problem is at the declaration of C.alsoCantReferToTypeVars, not at any call site.

Similarly, the instantiation of Map<T, S> above gives rise to warnings that T and S are malformed, but not to complaints that they are inappropriate type arguments to Map. We replace the malformed type arguments with dynamic, and so produce a Map<dynamic, dynamic>. We see that using a malformed type as a type argument in a parameterized type does not make the parameterized type malformed. The malformed type argument is instead replaced with dynamic which is the type argument used at runtime.

List<UndefinedType> l = <int>[3]; // warning, but works even in checked mode
List<int> list = <UndefinedType>[3]; // warning, but works even in checked mode

Here, the type of l is taken to be of List<dynamic>. A warning is issued that UndefinedType is not defined, but not about any mismatch between the types of the value and the variable. At runtime, checked mode will find that List<int> is a subtype of List<dynamic> and so the assignment does not cause a failure.

In the second line, the situation is reversed. The object being assigned is given type List<dynamic> because the malformed type UndefinedType is replaced by dynamic. Consequently, the object can be assigned to a List<int> (once again showing the unsoundness of generics).

5.9 Unsoundness

We are now in a position to review the various sources of unsoundness in the Dart type system. These are:

• Covariance of generics.

• Downcasting in the assignability rule (5.3).

• Assignability in function types.

• The interaction of privacy and interface types.

We discussed the first two points in Sections 5.3 and 5.5 respectively. The third point is really a product of the first two. Function types are naturally covariant in their return types but contravariant in their argument types. The same reasoning that dictated the use of covariance for generics applies to function types. This alone would make function subtyping an additional source of unsoundness.

In addition, the rules for assignability apply to all value transfers, including passing in function arguments and returning function results, which dictates the use of assignability for both argument and return types as discussed in Section 5.6.

The final source of unsoundness in Dart is one that we have not yet discussed—the interaction of ADT style privacy at the library level with interface-based types. To understand this interaction, let’s look at a variation on the stack example from the previous chapter. In this version, we will implement a class Stack so that we can instantiate multiple stacks:

library stack3;

class Stack {
 List _contents = [];
 get isEmpty => _contents.isEmpty;
 get top => isEmpty ? throw 'Cannot get top of empty stack' : _contents.last;
 get pop => isEmpty? throw 'Cannot pop empty stack' : _contents.removeLast();
 push(e) {
  _contents.add(e);
  return e;
 }
}


clone(Stack s) {
  Stack ns = new Stack();
  ns. contents = new List.from(s. contents);
  return ns;
}

We’ve added a clone function that takes a stack and produces another one with the same contents. This isn’t really a good idea, but it will suit our purpose here. Something that should raise a red flag is the fact that clone accesses the private variable contents of its formal parameter s. This is quite natural in a language that is designed around the notion of abstract data types (ADTs). In such a language, we are assured by the type system that any incoming parameter will be an instance of the class Stack that we have defined, and so will have a field named _contents.

And therein lies the rub: Dart is not designed around the notion of abstract data types. Dart is designed around the notion of interfaces. A type in Dart never denotes a specific implementation type. Rather, it denotes the interface of such a type. Anyone can implement the type Stack, and the implementation can be entirely different from the one defined in the class Stack. In particular, the implementation might not have a _contents field.

Indeed, suppose we decide to produce an alternative implementation of Stack on another library.

library stack4;

import 'stack3.dart' as stack show clone, Stack;

class Stack4 implements stack.Stack {
  final List _array = [];
  get isEmpty => _array.isEmpty;
  get top => isEmpty ? throw 'Cannot get top of empty stack' : _array.last;
  get pop => isEmpty? throw 'Cannot pop empty stack' : _array.removeLast();
  push(e) {
   _array.add(e);
   return e;
  }
}


main() {
 stack.clone(new Stack4());
}

Clearly, the call to stack.clone() will fail with a NoSuchMethodError when we try to access the non-existent _contents getter on the incoming parameter, which is an instance of Stack4 and indeed has no such getter. The question is whether the type system should complain about this code.

In a system based on ADTs, we might complain that Stack4 does not implement stack.Stack because it lacks the _contents accessor. This raises several difficulties:

• Since _contents is private to stack, its very existence must not be made known to other libraries. In particular, generating warnings in implementors of an interface is unacceptable. Adding a private member should be a purely internal decision of a class. If adding a private member to a class would spawn warnings in implementors of the class, this would not be possible.

• Setting the preceding objection aside, suppose we did issue a warning? What is the developer working on Stack4 to do? Defining a _contents accessor in Stack4 will not satisfy the requirement, because it is private to stack4 and cannot be accessed outside. The call inside clone will fail in exactly the same way. The _contents members in stack3 and stack4 are different. In short, no one outside stack3.Stack can ever implement a _contents getter that will implement the accessor defined inside stack3.

An alternative approach is to complain inside clone(). A variable of type Stack cannot reliably be assumed to have a private accessor such as _contents. Unfortunately, this would imply that no private instance members could ever be used on any object except this. Object-based encapsulation enforces this alternative. As we discussed in the previous chapter, Dart explicitly rejects object-based encapsulation.

In reality, no warnings are issued anywhere, and so again the system is unsound. This unsoundness is a direct consequence of the fact that the privacy discipline is based on ADTs whereas typing is based purely on interfaces.

Dart gives the programmer the option of defining abstract data types, in which case they must clearly document that the abstract types they are defining should not be implemented by others (though they can be extended by subclasses). One can define a single abstract datatype or a number of cooperating types in this style.

Alternatively, the programmer may opt to work in the preferred, interface based style, in which case private members should only be accessed via this, effectively following object-based encapsulation.

5.10 Related Work

The rigidity of type systems has provoked many attempts to relax it. In Scheme, a great deal of research was conducted on soft-typing, which was tied to type inference. More recent efforts in Racket mix typed and untyped code but at a relatively coarse granularity. Each module is either typed or untyped. Data crossing the boundaries between the typed and untyped units is dynamically checked. These checks are somewhat reminiscent of checked mode.

Dart’s optional type scheme is most closely related to the one developed by Strongtalk[16]. However, in Dart, optional typing is more tightly integrated into the language. Strongtalk did not reify generics and did not have a checked mode. Furthermore, warnings were the exclusive province of the IDE; compilation was completely independent of typechecking. Finally, Strongtalk had a sound type system with declaration side variance for generics.

The term optional typing was used in the context of Common Lisp, but denoted a somewhat different idea. Types were treated as valid assertions by the compiler, which leveraged them for optimization. However, these type annotations were not validated, making the system inherently unsafe. Dart requires pointer safety and implementations are not allowed to trust type annotations.

Dart’s mix of unsound generics and reification is unusual. The influence of Beta[17], where genericity is also covariant is clear. However, generics in Beta are not a special feature but instead are modeled by nested type members and so explicitly reified. In contrast, mainstream languages like Java and C# use sound type systems that include variance declarations. In Java, generics are not reified and variance is use based, whereas in C# generics are reified and variance is declaration based. Older designs such as C++ and Modula-3 use template-based expansion mechanisms.

5.11 Summary

Dart types are based on the concept of interfaces rather than implementation. Every class induces an interface that can be implemented by other classes, even if their implementations are unrelated.

Dart supports optional typing. Typed and untyped code may be freely intermixed. Dart type annotations do not effect runtime behavior, but uses of reified types naturally do. Dart, quite deliberately, does not have a sound type system. Instead, type rules are designed heuristically to balance flexibility and safety in the interests of developer productivity. Violations of the type rules lead to warnings that may be selectively disabled. Type warnings never preclude compilation and execution of the program.

During development, the use of checked mode allows type annotations to be interpreted as assertions, introducing dynamic type checks.

Dart includes generic types, which are treated covariantly. Generics and superinterface declarations are reified and can be tested at runtime.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.135.225