mirror of
https://github.com/dart-lang/sdk
synced 2024-09-05 00:13:50 +00:00
Newsletter 2017-08-25.
Review-Url: https://codereview.chromium.org/3003893002 .
This commit is contained in:
parent
c38c97def3
commit
8482c00ec3
248
docs/newsletter/20170825.md
Normal file
248
docs/newsletter/20170825.md
Normal file
|
@ -0,0 +1,248 @@
|
|||
# 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:
|
||||
``` dart
|
||||
class M1 {
|
||||
int foo() => 499;
|
||||
}
|
||||
```
|
||||
|
||||
New:
|
||||
``` dart
|
||||
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:
|
||||
|
||||
``` dart
|
||||
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:
|
||||
|
||||
``` dart
|
||||
abstract class A {
|
||||
int foo();
|
||||
}
|
||||
|
||||
class M1 extends A {
|
||||
int foo() => super.foo() + 1; // ERROR: super.foo() is abstract.
|
||||
}
|
||||
```
|
||||
|
||||
``` dart
|
||||
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 {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
``` dart
|
||||
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).
|
||||
|
||||
``` dart
|
||||
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.
|
||||
|
||||
``` dart
|
||||
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.
|
||||
|
||||
``` dart
|
||||
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:
|
||||
|
||||
``` dart
|
||||
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:
|
||||
|
||||
``` dart
|
||||
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.
|
||||
|
||||
``` dart
|
||||
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.
|
||||
|
Loading…
Reference in a new issue