Dart Language and Library Newsletter

2017-09-08 @floitschG

Welcome to the Dart Language and Library Newsletter.

Follow-Up - Call

Last newsletter we announced our desire to remove the call operator from the language. We got some feedback that showed some uses in the wild. Please keep them coming. It will definitely influence our decision whether (or when) we are going to remove the operator.

We also forgot an additional benefit of removing the operator: since users wouldn't be able to implement functions by themselves we could extend the Function interface with useful properties. For example, we could add getters that return the arity / signature of the receiver closure:

void onError(Function errorHandler) {
  // positionalArgumentCount includes optional positional parameters.
  if (errorHandler.positionalParameterCount >= 2) {
    errorHandler(error, stackTrace);
  } else {
    errorHandler.positionalParameterCount == 1);
    errorHandler(error);
  }
}

Fuzzy Arrow

In Dart 1.x dynamic was used for both the top and bottom of the typing hierarchy. Depending on the context, dynamic could either mean Object (top) or Null (bottom). For the remainder of the section remember that every type is a subtype of Object (which is why it's called “top”), and every type is a supertype of Null.

This schizophrenic interpretation of dynamic can be observed easily with generic types:

void main() {
  print(<int>[1, 2, 3] is List<dynamic>);  // Use `dynamic` as bottom. => true.
  print(<dynamic>[1, 2, 3] is List<int>);  // Use `dynamic` as top. => true.
}

In the first statement, List<dynamic> is used as a supertype of List<int>, whereas in the second statement, List<dynamic> is used as subtype of List<int>. This works for every type and not just int. As such, dynamic clearly is top and bottom at the same time.

With strong mode, this dual-view of dynamic became an issue, and, for the sake of soundness, dynamic was downgraded to Object. It still supports dynamic calls, but can't be used as bottom anymore. In strong mode, the second statement thus prints “false”. However, strong mode kept one small exception: fuzzy arrows.

The fuzzy arrow exception allows dynamic to be used as if it was bottom when it is used in function types. Take the following example:

/// Fills [list2] with the result of applying [f] to every element of
/// [list1] (if [f] is of arity 1), or of applying [f] to every
/// element of [list1] and [list2] (otherwise).
void map1or2(Function f, List list1, List list2) {
  for (int i = 0; i < list1.length; i++) {
    var x = list1[i];
    if (f is Function(dynamic)) {
      list2[i] = f(x);
    } else {
      var y = list2[i];
      list2[i] = f(x, y);
    }
  }
}

int square(int x) => x * x;

void main() {
  var list1 = <int>[1, 2, 3];
  var list2 = new List(3);
  map1or2(square, list1, list2);
  print(list2);
}

This code is relatively dynamic and avoids lots of types (and in particular generic arguments to map1or2), but it is a correct strong mode program. In DDC it prints [1, 4, 9].

There are some implicit dynamics in the program, but we are really interested in the one explicit dynamic in the function-type test: if (f is Function(dynamic)). Intuitively, that test looks good: we don‘t mind which type the function takes and thus wrote dynamic for the parameter type. However, that wouldn’t work if dynamic was interpreted as Object. In that case, the is check asks whether the provided f could be invoked with any Object. That‘s not what we want. We don’t want to invoke it with a random object value that we found somewhere, but invoke it with the values from list1. It‘s the caller’s responsibility to make sure that the types match. In fact, we don't care for the type at all. The is check is just here to test for the arity.

Since checking for arity is a common pattern, strong mode still treats dynamic as bottom in this context. This function types is thus equivalent to an arity check.

For a long time, the fuzzy arrow exception was necessary. Dart didn‘t have any other way to do arity checking. Only with the move of the Null type to the bottom of the typing hierarchy, was it possible to explicitly use the bottom type instead of just dynamic. A sounder way of asking for a function’s arity is thus:

if (f is Function(Null)) {

This can be read as: “is f a function that takes at least null?”. Without non-nullable types every 1-arity function takes null and this test is equivalent to asking whether the function takes one argument.

<footnote> With non-nullable types, the bottom type would need to change, since there are types that wouldn‘t accept null anymore. At that point we would need to introduce a Nothing type, and the is-check would need to be rewritten to if (f is Function(Nothing)). Admittedly, the spoken interpretation doesn’t sound as logical anymore: “is f a function that takes at least Nothing?” </footnote>

Since there is now a “correct” way of testing for the arity of functions, the language team recently started to investigate whether we could drop the fuzzy arrow exception from strong mode (and thus Dart 2.0).

Although the removal of fuzzy errors leads to breakages, our experience is pretty positive so far. The biggest problems arise in cases where the current type system is too weak to provide a correct replacement. Among those, Map.fromIterables clearly makes the biggest problems. The old signature of that constructor is Map.fromIterable(Iterable iterable, {K key(element), V value(element)}). Implicitly, both functions for key and value take dynamic arguments and use the fuzzy arrow exceptions to support iterables of any kind.

Without the fuzzy arrow exception the implicit dynamic in those types is read similar to Object, thus requiring users to provide functions that can deal with any object (and not just the ones from the iterable).

Unfortunately, our trick of replacing the dynamic with Null doesn't work here:

Map.fromIterable(Iterable iterable,
    {K key(Null element), V value(Null element)}) {
  ...
}

// Works when the argument is not a function literal:
new Map<int, String>.fromIterable(["1", "2"], keys: int.parse);

// Doesn't work, with function literal:
new Map<int, String>.fromIterable([1, 2], values: (x) => x.toString());

The reason the second instantiation doesn't work is that Dart uses the context of a function literal to infer the parameter type. In this case the literal (x) => x.toString() is used in a context where a V Function(Null) is expected, and the literal is thus automatically adapted to satisfy this signature: String Function(Null). However, that means that any invocation of this function with a value that is not null yields to a dynamic error.

The correct way to fix this constructor is to allow generic arguments for constructors:

Map.fromIterable<T>(Iterable<T> iterable,
    {K key(T element), V value(T element)}) {
  ...
}

Supporting generic arguments for constructors is on our roadmap, but will not make it for Dart 2.0. In the meantime we either have to live with requiring functions that take objects, or we will have to change the key and value type annotation to Function, thus losing the arity and type information:

Map.fromIterable(Iterable iterable, {Function key, Function value}) {
  ...
}

Enhanced Type Promotion

As mentioned in a previous newsletter: one of our goals is to improve Dart's type promotion. We want to make better use of is and is! checks. For example, promote x to int after the if in the following code: if (x is! int) throw x;.

When the language team discussed this topic we looked at the conditions under which type promotion would be useful and intuitive. One of the current restrictions is that promoted variables may not be assigned again:

void foo(Object x) {
  if (x is String) {
    x = x.subString(1);  // Error: subString is not defined for Object.
    print(x + "suffix");
  }
}

void bar(Object x) {
  if (x is WrappedInt) {
    x = x.value;   // Error: `value` is not defined for Object.
  }
  assert(x is int);
}

As can be seen in these two examples, assignments would require an analysis that deals with flow-control, and that assigns potentially different types to the same variable. Inside foo the user wants to continue using x as String, whereas in bar the user wants to use x as an Object after the assignment.

We have discussed multiple approaches to provide the correct, intuitive behavior in these cases, and we are confident that we can provide a solution that will work in most cases. However, we don't want to delay or block the “easy” improvements, and therefore decided to exclude assignments from the current proposal. We will come back to assignments of promoted variables in the future.