Review-Url: https://codereview.chromium.org/3003893002 .
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:
- it's unintuitive for our users,
A.foo
has to be concrete (sinceM1.foo
uses it with asuper.foo
call).M1
can only depend on one supertype,- there is no obvious way to compose mixins (since
extends
is already taken). - there is no enforcement, that the class that mixes in
M1
actually has a non-abstract implementation ofA
. - 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 Converter
s (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.