dart-sdk/docs/newsletter/20171110.md
Florian Loitsch 145c9a5d56 Newsletter 2017-11-10.
Change-Id: Ibecaf1e42105093193897e2ee57ed24d34c5e7b8
Reviewed-on: https://dart-review.googlesource.com/20200
Reviewed-by: Florian Loitsch <floitsch@google.com>
2017-11-10 18:09:30 +00:00

11 KiB

Dart Language and Library Newsletter

2017-11-10 @floitschG

Welcome to the Dart Language and Library Newsletter.

Did You Know?

Constructors

Dart has many ways to make writing constructors easier or more powerful. The most known is probably the concise syntax for initializing instance fields directly in the signature line (see below). This section shows some other, less known features.

// Concise syntax for initializing fields while declaring parameters.
class A {
  final int x;
  A(this.x);
}

Generative Constructors

A constructor is "generative", if it is called on a freshly created instance to initialize the object. This sounds complicated, but just describes the behavior of the most common constructors.

class A {
  int x;
  A(int y) : this.x = y + 2;
}

When a user writes new A(), conceptually, the program first instantiates an uninitialized object of type A, and then lets the constructor initialize it (set the field x).

The reason for this wording is, that generative constructors can be used in super calls in initializer lists. When called as super the generative constructor doesn't instantiate a new object again. It just does its part of the initialization.

class A {
  int x;
  A(int y) : this.x = y + 2;
}

class B extends A {
  B(int z) : super(z - 1) {
    print("in B constructor");
  }
}

The order of evaluation is well defined: first all expressions in the initializer list are evaluated. Then the initializer list of the super constructor is run. This continues, until Object (the superclass of every class) is reached. Then, the bodies of the constructors are executed in reverse order, first starting the one from Object (not doing anything), and working its way down the class hierarchy.

This evaluation order is usually not noticeable, but can be important when the expressions have side-effects, and/or the bodies read final fields:

int _counter = 0;

class A {
  final int aCounter;
  A() : aCounter = _counter++ {
    print("foo: ${foo()}");
  }
}

class B extends A {
  final int bCounter;
  final int field;

  B()
      : field = 499,
        bCounter = _counter++ {
    print("B");
  }

  int foo() => field;
}

main() {
  var b = new B();
  print("aCounter: ${b.aCounter}");
  print("bCounter: ${b.bCounter}");
}

Running this program yields:

foo: 499
B
aCounter: 1
bCounter: 0

Note that the bCounter expression is evaluated first, yielding 0, and that aCounter, coming second, is set to 1. Furthermore, the final field field in B is set to 499 when the constructor in A indirectly accesses the field.

Dart guarantees that final fields are only visible with their final value. Dart ensures this property by splitting the construction of objects into two: the initializer list, and the constructor body. Without this two-phase initialization Dart wouldn't be able to provide this guarantee.

Factory Constructors

Factory constructors are very similar to static functions, except that they can be invoked with new. They don't work on an instantiated (uninitialized) object, like generative constructors, but they must create the object themselves.

The following example shows how Future.microtask could be implemented with a factory and the existing Completer class.

class Future<T> {
  factory Future.microtask(FutureOr<T> computation()) {
    Completer c = new Completer<T>();
    scheduleMicrotask(() { ... c.complete(computation()) ... });
    return c.future;
  }
}

The actual implementation uses private classes to be more efficient, but is otherwise very similar to this code.

Factory constructors cannot be used as targets of super in initializers. (This also means that a class that only has factory constructors cannot be extended).

Redirecting Generative Constructor

When constructors want to share code it is often convenient to just forward from one constructor to another one. This can be achieved with factory constructors, but if the constructor should also be usable as the target of a super-initializer call, then factory constructors (as described above) are not an option. In this case, one has to use redirecting generative constructors:

class Point {
  final int x;
  final int y;
  Point(this.x, this.y);
}

class Rectangle {
  int x0;
  int y0;
  int x1;
  int y1;

  Rectangle.coordinates(this.x0, this.y0, this.x1, this.y1);

  Rectangle.box(Point topLeft, int width, int height)
      : this.coordinates(topLeft.x, topLeft.y, topLeft.x + width, topLeft.y.height);
}

class Square extends Rectangle {
  Box(Point topLeft, int width) : super.box(topLeft, width, width);
}

The Rectangle class has two constructors (both generative): coordinates and box. The box constructor redirects to the coordinates constructor.

As can be seen, a subtype, here Square, can still use the constructor in the initializer list.

Redirecting Factory Constructors

Frequently, factory constructors are just used to instantiate a differently named class. For example, the Iterable class is actually abstract and a new Iterable.empty() can't therefore be generative but must be a factory. With factory constructors this could be implemented as follows:

abstract class Iterable<E> {
  factory Iterable.empty() {
    return new _EmptyIterable<E>();
  }
}

There are two reasons, why we are not happy with this solution:

  1. there is an unnecessary redirection: the compilers need to inline the factory constructor, instead of seeing directly that a new Iterable.empty() should just directly create an _EmptyIterable. (Our compilers inline these simple constructors, so this is not a real problem in practice).
  2. A factory constructor with a body cannot be const. Clearly, there is code being executed (even if it's just new _EmptyIterable()), which is not allowed for const constructors.

The solution is to use redirecting factory constructors:

abstract class Iterable<E> {
  const factory Iterable.empty() = _EmptyIterable<E>;
}

Now, the Iterable.empty() constructor is just a synonym for _EmptyIterable<E>. Note that we don't even need to provide arguments to the _EmptyIterable<E> constructor. They must be the same as the one of the redirecting factory constructor.

Another example:

class C {
  final int x;
  final int y;
  const C(this.x, this.y);
  factory const C.duplicate(int x) = _DuplicateC;
}

class _DuplicateC implements C {
  final int x;
  int get y => x;
  const _DuplicateC(this.x);
}

Shorter Final Variables

In Dart it is now easier to declare mutable locals, than to declare immutable variables:

var mutable = 499;
final immutable = 42;

Declaring a variable as mutable, but not modifying it, isn't a real problem per se, but it would be nice, if the var keyword actually expressed the intent that the variable will be modified at a later point.

We recently looked at different ways to make immutable locals more appealing. This section contains our proposal.

Instead of using a different keyword (like val) we propose to use an even shorter syntax for immutable locals: colon-equals (:=).

In this proposal, a statement of the form identifier := expression; introduces a new final local variable.

  // DateTime.toString() method.
  String toString() {
    y := _fourDigits(year);
    m := _twoDigits(month);
    d := _twoDigits(day);
    h := _twoDigits(hour);
    min := _twoDigits(minute);
    sec := _twoDigits(second);
    ms := _threeDigits(millisecond);
    us := microsecond == 0 ? "" : _threeDigits(microsecond);
    if (isUtc) {
      return "$y-$m-$d $h:$min:$sec.$ms${us}Z";
    } else {
      return "$y-$m-$d $h:$min:$sec.$ms$us";
    }
  }

As a first reaction, it feels dangerous to just use one character (":") to introduce a new variable. In our experiments this was, however, not an issue. In fact, single-character modifiers of = are already common: x += 3 is also just one character on top of = and we are not aware of any readability issues with compound assignments. Furthermore, syntax highlighting helps a lot in ensuring that these variable declarations aren't lost in the code.

We would also like to support typed variable declarations: Type identifier := expression. (The following examples are just random variable declarations of our codebase that have been rewritten to use the new syntax).

int pos := value.indexOf(":");
JSSyntaxRegExp re := pattern;
IsolateEmbedderData ied := isolateEmbedderData.remove(portId);

For now, we are only looking at the := syntax for local variables. If it proves to be successful, we will investigate whether we should allow the same syntax for final (global) statics or fields.

For Loops

For loops are another place where users frequently declare new variables. There, we need to pay a bit more attention. For example, the for-in statement doesn't even have any assignment symbol, which we could change to :=.

When looking at uses of for-in, we found that these loops are almost never used without introducing a loop variable:

var x;
for (x in [1, 2]) {
  print(x);
}

In fact, the only cases where we found this pattern was in our own tests...

We thus propose to change the meaning of for (identifier in Iterable). It should become syntactic sugar for for (final identifier in Iterable).

Note that Dart already supports final identifier in for-in loops, since each iteration has its own variable. This can be seen in the following example:

main() {
  var funs = [];
  for (final x in [1, 2, 3]) {  // With or without `final`.
    funs.add(() => x);
  }
  funs.forEach((f) => print(f()));  // => 1 2 3
}

With the new syntax the final keyword wouldn't be necessary in this example.

Finally, we also had a look at for. Similar to for-in, a for loop, already now, does not reuse the loop variable, but introduces a fresh variable for each iteration.

main() {
  var funs = [];
  for (int i = 0; i < 3; i++) {
    funs.add(() => i);
  }
  funs.forEach((f) => print(f()));  // => 0 1 2
}

This means that there is already syntactic sugar happening to make this happen. It is thus relatively straightforward to support a version where a loop variable introduced with := is final within the body of the loop.

main() {
  var funs = [];
  for (i := 0; i < 3; i++) {
    funs.add(() => i);
  }
  funs.forEach((f) => print(f()));  // => 0 1 2
}

This would be (roughly) equivalent to:

main() {
  var funs = [];
  var i_outer;
  for (i_outer = 0; i_outer < 3; i_outer++) {
    i_inner := i_outer;
    funs.add(() => i_inner);
  }
  funs.forEach((f) => print(f()));  // => 0 1 2
}

Summary

We are investigating ways to make the declaration of final locals easier. In this proposal we suggest the use of := as new syntax to concisely declare a fresh final local.

We also propose changes to the for and for-in statements to make the declaration of final variables concise. The for loop would support the := syntax, and a for-in statement without var or type would implicitly introduce a fresh final variable.