dart-sdk/docs/newsletter/20170825.md
2017-08-25 17:24:27 +02:00

11 KiB

Dart Language and Library Newsletter

2017-08-25 @floitschG

Welcome to the Dart Language and Library Newsletter.

Under Active Development

This section provides updates to the areas we are actively working on as part of long-running efforts. Many of these sections have a more detailed explanation in a previous newsletter.

Better Organization

Goal: collect and organize the documents (proposals, ...) the Dart language team produces.

Since last time, a proposal for an improvement to mixins has been added: https://github.com/dart-lang/sdk/blob/master/docs/language/informal/mixin-declaration.md

See below for a summary of this proposal.

Mixins

In Dart every class that satisfies some restrictions can be used as a mixin. While this approach is quite elegant, it is not how developers think of mixins. This leads to errors, makes normal classes abnormally brittle (since, in theory, minor changes could break their mixin shape) and makes it hard to lift restrictions (such as compositions and requirements on super interfaces).

We are therefore looking at making mixins separate from normal classes. Instead of introducing a mixin with class, one should use mixin instead:

Old:

class M1 {
  int foo() => 499;
}

New:

mixin M1 {
  int foo() => 499;
}

Since mixins now have their own syntax, we can fix some of the problems we encountered with the class syntax. Specifically, invoking super methods inside a mixin was a problem. Take for example, a mixin that wants to be mixed in on top of a class that implements A, so that it can wrap calls to A's foo method. The current specification (which has only been implemented in the VM) uses the extends clause to enforce this requirement:

class A {
  int foo() => 499;
}

// Old style mixin:
class M1 extends A {
  int foo() => super.foo() + 1;
}

class B extends A with M1 {}

The extends approach has multiple problems:

  1. it's unintuitive for our users,
  2. A.foo has to be concrete (since M1.foo uses it with a super.foo call).
  3. M1 can only depend on one supertype,
  4. there is no obvious way to compose mixins (since extends is already taken).
  5. there is no enforcement, that the class that mixes in M1 actually has a non-abstract implementation of A.
  6. requiring supertypes that have constructors is not possible, since mixins currently must not have constructors.

The following code snippets illustrate some of these problems with examples:

abstract class A {
  int foo();
}

class M1 extends A {
  int foo() => super.foo() + 1;  // ERROR: super.foo() is abstract.
}
abstract class A {
  int foo();
}
abstract class B {
  String bar();
}

// C does *not* know AB (below), nor M1 (also below).
class C implements A, B {
  ...
}

// Workaround class to make it possible to require multiple super classes.
class AB implements A, B {
  int foo() => null;
  String bar() => null;
}

class M1 extends AB {
  int foo() => super.foo() + 1;
  String bar() => super.bar() + "_string";
}

// Intermediate workaround class that adds `AB` on top of C.
class _C extends C implements AB {}

// D needs to extend _C since M1 requires AB as superclass.
class D extends _C with M1 {
  ...
}
class A {
  int foo() => 499;
}

class M1 extends A {
  int foo() => super.foo() + 1;
}

abstract class B implements A {}

class C extends B with M1 {}  // No error, since `B` *implements* A.

All of these problems are easily solved with the specialized mixin syntax. Instead of using extends to specify the supertype requirements, mixin declarations use requires, which can take multiple supertypes.

Furthermore, since mixins are separate from classes, it is perfectly fine to do super invocations (inside the mixin) to abstract methods. Only at mixin time does the language check that the required interfaces are fully implemented (or at least the methods that are used by the mixin).

abstract class A {
  int foo();
}

class B {
  String bar() => "bar";
}

abstract class C extends B implements A {
}

mixin M1 requires A, B {
  int foo() => super.foo() + 1;
  String bar() => super.bar() + "_string";
}

class D extends C with M1 {}  // ERROR: foo is not implemented.

class C2 extends B implements A {
  int foo() => 42;
}

class D2 extends C2 with M1 {} // OK.
}

The current proposal doesn't add any additional features yet, but it lends itself to allowing composition with extends, and supporting constructors.

Corner Cases

A lot of the work the language team does is to discuss and solve corner cases that most users never encounter. In this section we show two of the more interesting ones.

Inference vs Manual Types - Part 1.

Dart 2.0 strongly relies on type inference to make programming easier. At the same time, Dart still allows programmers to write types by hand. This section explores some interesting interactions between written and inferred types.

The following example demonstrates how explicitly writing a type can break a program; or make it work.

void foo(List<int> arg) { ... }

var x = [1, 2];  // Inferred as `List<int>`.
List<Object> y = [1, 2];
print(x.runtimeType);  // => List<int>.
print(y.runtimeType);  // => List<Object>.
foo(x);
foo(y); // Error.
x.add("string");  // Error.
y.add("string");  // OK.

In the case of x the type inference infers that [1, 2] is a list of int. The variable y, on the other hand, is typed as List<Object> and that type is pushed down to the expression [1, 2]. Those two list-literals have thus two completely different dynamic types despite having the same textual representation.

Both cases are useful: the more precise List<int> is used to call foo, whereas, y (being a List<Object>) can store objects with types different than int.

Writing a type, different than the one that type inference finds, can thus change the behavior in significant ways (and not necessarily in bad ways). Interestingly, writing the exact same type may also lead to different behavior. That is, given a variable declaration var x = someExpr, there are cases where Type x = someExpr, with Type being the type that would have been inferred for someExpr, leads to a different program.

I have split this section into two parts, so that developers familiar with strong mode inference have the opportunity to search for one of these expressions. The second part is just after the next section.

Function Types and Covariant Generics

To make life easier for developers, Dart allows covariant generics. In short, Dart says that a List<Apple> is a List<Fruit>. As long as the value is only read out of the list, that is perfectly fine. However, after assigning a List<Apple> to a List<Fruit> we cannot simply add a Banana to the list. Statically, adding a Banana is fine (after all, it's a List<Fruit>), but dynamically Dart adds a check to ensure that this can't happen.

List<Apple> apples = <Apple>[new Apple()];
List<Fruit> fruits = apples;  // OK because of covariant generics.
fruits.add(new Banana());  // Statically ok, but dynamic error.

In practice this works fine, since most generic types are used as "out" types. The exceptions, such as Converters (where the first generic is used as input) are fortunately very rare (although quite annoying when users hit them).

As mentioned above, Dart adds checks whenever the input might not be the correct type to ensure that the heap stays sound. For example, the add method is conceptually compiled to:

void add(Object o) {
  if (o is! T) throw new TypeError("$o is not of type $T");
  /* Add `o` to the list. */
}

Some of these checks are easy to find, but some are much trickier:

class A<T> {
  Function(T) fun;
}

main() {
  A<int> a = new A<int>();
  a.fun = (int x) => x + 3;
  A<Object> a2 = a;         // #0
  var f = a2.fun;           // #1, static type: Function(Object).
  print(f('some_string'));  // #2
}

At #0 the A<int> is assigned to an A<Object>. Because of covariant generics, this assignment is allowed. The static type of a2.fun is Function(Object), which is inferred for f at #1. However, the function that was stored in a.fun is a function that only takes integers. As such, the call at line #2 must not succeed, and Dart must insert checks to ensure that the x + 3 is never invoked with a String.

The safest and easiest choice would be to insert a check for int in the closure (int x) => x + 3. However, that would be too inefficient. Every closure would need to check its arguments, even if it is never used in a situation where it might receive arguments of the wrong type. Dart implementations could cheat, and store a hidden bit on closures that enables argument checks or not, but things would get complicated fast.

There isn't any place to add checks inside A. Clearly, users must be able to access the member fun, and A itself can't know how (and as which type) the returned member will be used.

This only leaves the assignment in line #1. Dart has to insert a check here that ensures that the closure that is returned from A.fun has the correct type. This is exactly, what Dart 2.0 will do. A dynamic check for the assignment ensures that the dynamic type of a2.fun matches the inferred type of f. Since, in this example, int Function(int) (the type of the closure) is not assignable to dynamic Function(Object) (the inferred type of f), the line #2 is never reached.

Afaik this is the only case where a declaration of the form var x = expr; fails dynamically when assigning the evaluated expression to the value of the inferred type. It's not that the type inference did a bad job, but that there was no earlier place to inserts the checks that Dart has to do to ensure heap soundness.

Note that this check hasn't been added to DDC, yet.

Inference vs Manual Types - Part 2

In part 1 of this section we finished with a claim that there are expressions for which writing the inferred type at their declaration point yields a different program than if we had left it off.

The easiest way to get such an expression is to use the fact that Dart uses down and upwards inference, but doesn't iterate the inference process.

var x = [[499], [false]];  // Inferred to be a List<List<Object>>.
List<List<Object>> y = [[499], [false]];

x[0].add("str");  // Error.
y[0].add("str");  // OK.

For x, the nested list [499] is inferred as List<int>, and [false] is inferred as List<bool>. The least upper bound of these two types is List<Object> and the surrounding list (and thus x) is inferred to be List<List<Object>>.

For y, the List<List<Object>> type is used in the downwards inference to type the right-hand side of the assignment. The whole outer list is typed as List<List<Object>> (similar to the one for x) without even looking at the elements. The type is then continued to be pushed to the entries of the list. Instead of using up inference to infer that [499] is a List<int> the context already provides the type that this expression should have: List<Object>. All entries of the outer list are forced to be List<Object>.

When the program later tries to add a string ("str") to the first list-entries, the one that was inferred to be a List<int> has to dynamically reject that value, whereas the one that was forced to be List<Object> succeeds.