Dart Language and Library Newsletter

2017-09-01 @floitschG

Welcome to the Dart Language and Library Newsletter.

The Case Against Call

Dart 1.x supports callable objects. By adding a call method to a class, instances of this class can be invoked as if they were functions:

class Square {
  int call(int x) => x * x;
  toString() => "Function that squares its input";
}

main() {
  var s = new Square();
  print(s(4));  // => 16.
  print(s);  // => Function that squares its input.
  print(s is int Function(int));  // => true.
}

Note that Square doesn't need to implement any Function interface: as soon as there is a call method, all instances of the class can be used as if they were closures.

While we generally like the this feature (let‘s be honest: it’s pretty cool), the language team is trying to eventually remove it from the language. In this section, we explain the reasons for this decision.

Wrong Name

Despite referring to the feature as the “call operator”, it is actually not implemented as an operator. Instead of writing the call operator similarly to other operators (like plus, minus, ...), it's just a special method name.

As an operator we would write the Square class from above as follows:

class Square {
  int operator() (int x) => x * x;
}

Some developers actually prefer the “call” name, but the operator syntax wouldn't just be more consistent. It would also remove the weird case where we can tear off call methods infinitely:

var s = new Square();
var f = s.call.call.call.call.call.call;
print(f(3));  // => 9;

If the call operator was an actual operator, there wouldn't be any way to tear off the operator itself.

Tear-Offs are Too Good

Tearing off a function is trivial in Dart. Simply referring to the corresponding method member tears off the bound function:

class Square {
  int square(int x) => x * x;
}

main() {
  var s = new Square();
  var f = s.square;
  print(f(3));  // => 9.
}

The most obvious reason for a call-operator is to masquerade an instance as a function. However, with easy tear-offs, one can just tear off the method and pass that one instead. The only pattern where this doesn't work, is if users need to cast a function type back to an object, or if they rely on specific hashCode, equality or toString.

The following contrived example shows how a program could use these properties.

// The `Element` class and the `disposedElements` getter are provided
// by some framework.

/// An element that reacts to mouse clicks.
class Element {
  /// The element's click handler is a function that takes a `MouseEvent`.
  void Function(MouseEvent) clickCallback;
}

/// A stream that informs the user of elements that have been disposed.
Stream<Element> disposedElements = ...;

// ============= The following code corresponds to user code. =====

// Attaches a click handler to the element of the given name
// and writes the clicks to a file.
void logClicks(String name) {
  var sink = new File("$name.txt").openWrite();
  var element = screen.getElement(name);
  element.clickCallback = sink.writeln;
}

main() {
  logClicks('demo');
  logClicks('demo2');
  disposedElements.listen((element) {
    // Would like to close the file for the registered handlers.
    // ------
  });
}

In the beginning of main the program registers some callbacks on UI elements. However, when these elements are disposed of, the program currently does not know how to find the IOSink that corresponds to the element that is removed.

One easy solution is to add a global map that stores the mapping between the elements and the opened sinks. Alternatively, we can introduce a callable class that stores the open file:

// A class that connects the open output file with the handlers.
class ClickHandler {
  final IOSink sink;
  ClickHandler(this.sink);
  void call(Object event) {
    sink.writeln(event);
  }
}

// Attaches a click handler to the element of the given name
// and writes the clicks to a file.
void logClicks(String name) {
  var sink = new File("$name.txt").openWrite();
  var handler = new ClickHandler(sink);
  var element = screen.getElement(name);
  // Uses the callable object as handler.
  element.clickCallback = handler;
}

main() {
  logClicks('demo');
  logClicks('demo2');
  disposedElements.listen((element) {
    // ============
    // Casts the function back to a `ClickHandler` class.
    var handler = element.clickCallback as ClickHandler;
    // Now we can close the sink.
    handler.sink.close();
  });
}

By using a callable class, the program can store additional information with the callback. When the framework tells us which element has been disposed, the program can retrieve the handler, cast it back to ClickHandler and read the IOSink out of it.

Fortunately, these patterns are very rare, and usually there are many other ways to solve the problem. If you know real world programs that require these properties, please let us know.

Typing

A class that represents, at the same time, a nominal type and a structural function type tremendously complicates the type system.

As a first example, let's observe a class that uses a generic type as parameter type to its call method:

class A<T> {
  void call(T arg) {};
}

main() {
  var a = new A<num>();
  A<Object> a2 = a;  // OK.
  void Function(int) f = a;  // OK.
  // But:
  A<int> a3 = a;  // Error.
  void Function(Object) f2 = a;  // Error.
}

Because Dart's generic types are covariant, we are allowed to assign a to a2. This is the usual List<Apple> is a List<Fruit>. (This is not always a safe assignment, but Dart adds checks to ensure that programs still respect heap soundness.)

Similarly, it feels natural to say that a which represents a void Function(T), with T equal to num, can be used as a void Function(int). After all, if the method is only invoked with integers, then the num is clearly good enough.

Note that the assignment to a2 uses a supertype (Object) of num at the left-hand side, whereas the assignment to f uses a subtype (int). We say that the assignment to a2 is covariant, whereas the assignment to f is contravariant on the generic type argument.

Our type system can handle these cases, and correctly inserts the necessary checks to ensure soundness. However, it would be nice, if we didn't have to deal with objects that are, effectively, bivariant.

Things get even more complicated when we look at subtyping rules for call methods. Take the following “simple” example:

class C {
  void call(void Function(C) callback) => callback(this);
}

main() {
  C c = new C();
  c((_) => null);  // <=== ok.
  c(c);  // <=== ok?
}

Clearly, C has a call method and is thus a function. The invocation c((_) => null) is equivalent to c.call((_) => null). So far, things are simple. The difficulty arises when c is passed an instance of type C (in this case c itself).

The type system has to decide if an instance of type C (here c) is assignable to the parameter type. For simplicity, we only focus on subtyping, which corresponds to the intuitive “Apple” can be assigned to “Fruit”. Usually, subtyping is written using the “<:” operator: Apple <: Fruit. This notation will make this text shorter (and slightly more formal).

In our example, the type system thus wants to answer: C <: void Function(C)? Since C is compared to a function type, we have to look at C's call method and use that type instead: void Function(void Function(C)). The type system can now compare these types structurally: void Function(void Function(C)) <: void Function(C)?

It starts by looking at the return types. In our case these are trivially assignable: both are void. Next up are the parameter types: void Function(C) on the left, and C on the right. Since these types are in parameter position, we have to invert the operands. Formally, this inversion is due to the fact that argument types are in contravariant position. Intuitively, it's easy to see that a fruit function (Function(Fruit)) can always be used in places where an apple function (Function(Apple)) is required: Function(Fruit) <: Function(Apple) because Apple <: Fruit.

Getting back to our example, we had just concluded that the return types of void Function(void Function(C)) <: void Function(C) matched and were looking at the parameter types. After switching sides we have to check whether C <: void Function(C).

If this looks familiar, you paid attention: this is the question we tried to answer in the first placeā€¦

Fundamentally, this means that Dart (with the call method) features recursive types. Depending on the resolution algorithm of the type system we can now either conclude that:

  • C <: void Function(C), if we use a co-inductive algorithm that tracks recursion (which is just fancy wording for saying that we assume everything works and try to see if things break), or
  • C </: void Function(C), if we use an inductive algorithm that tracks recursion. (Start with nothing, and build up the truth).

This is just one out of multiple issues that call methods bring to Dart's typing system. Fortunately, we are not the first ones to solve these problems. Recursive type systems exist in the wild, and there are known algorithms to deal with them (for example Amadio and Cardelli http://lucacardelli.name/Papers/SRT.pdf), but they add lots of complexity to the type system.

Conclusion

Given all the complications the call method, the language team intends to eventually remove this feature from the language.

Our plan was to slowly phase call methods out over time, but we are now investigating, if we should take the jump with Dart 2.0, so that we can present a simpler type system for our Dart 2.0 specification.

At this stage we are still collecting information, including looking at existing programs, and gathering feedback. If you use this feature and don't see an easy work-around please let us know.

Limitations on Generic Types

A common operation in Dart is to look through an iterable, and only keep objects of a specific type.

class A {}
class B extends A {}

void main() {
  var itA = new Iterable<A>.generate(5, (i) => i.isEven ? new A() : new B());
  var itB = itA.where((x) => x is B);
}

In this example, itA is an Iterable that contains both As and Bs. The where method then filters these elements and returns an Iterable that just contains Bs. It would thus be great to be able to use the returned Iterable as an Iterable<B>. Unfortunately, that's not the case:

print(itB is Iterable<B>);  // => false.
print(itB.runtimeType);  // => Iterable<A>.

The dynamic type of itB is still Iterable<A>. This becomes obvious, when looking at the signature of where: Iterable<E> where(bool test(E element)) (where E is the generic type of the receiver Iterable).

It's natural to wonder if we could improve the where function and allow the user to provide a generic type when they want to: itA.where<B>((x) => x is B). If the user provides a type, then the returned iterable should have that generic type. Otherwise, the original type should be used:

// We would like the following return types:
var anotherItA = itA.where(randomBool);  // an Iterable<A>.
var itB = itA.where<B>((x) => x is B);  // an Iterable<B>.

The signature of where would need to look somehow similar to:

Iterable<T> where<T>(bool test(E element));

This signature would work for the second case, where the user provided a generic argument to the call, but would fail for the first case. Since there is no way for the type inference to find a type for the generic type, it would fill that type with dynamic. So, anotherItA would just be an Iterable<dynamic> and not Iterable<A>.

The only way to provide “default” values for generics is to use the extends clause such as:

Iterable<T> where<T extends E>(bool test(E element));

This is because Dart's type inference uses the bound of a generic type when no generic argument is provided.

Running our tests, this looks promising:

var anotherItA = itA.where(randomBool);
print(anotherItA.runtimeType);  // => Iterable<A>.

var itB = itA.where<B>((x) => x is B);
print(itB.runtimeType);  // => Iterable<B>.

Clearly, given the title of this section, there must be a catch...

While our simple examples work, adding this generic type breaks down with covariant generics (List<Apple> is a List<Fruit>). Let's try our new where function on a more sophisticated example:

int nonNullLength(Iterable<Object> objects) {
  return objects.where((x) => x != null).length;
}

var list = [1, 2];  // a List<int>.
print(nonNullLength(list));

The nonNullLength function just filters out all elements that are null and returns the length of the resulting Iterable. Without our update to the where function this works perfectly. However, with our new function we get an error.

The where in nonNullLength has no generic argument, and the type inference has to fill it in. Without any provided generic argument and no contextual information, the type inference uses the bound of the generic parameter. For our improved where function the generic parameter clause is T extends E and the bound is thus E. Within nonNullLength the provided argument objects is of type Iterable<Object> and the inference has to assume that E equals Object. The compiler statically inserts Object as generic argument to where.

Clearly, Object is not a subtype of int (the actual generic type E of the provided Iterable). As such, a dynamic check must stop the execution and report an error. In Dart 2.0 the nonNullLength function would therefore throw.

Type inference is only available in strong mode and Dart 2.0, and, so far, only DDC supports the new type system. (Also, this particular check is only implemented in a very recent DDC.) Eventually, all our tools will implement the required checks.

Without actual default values for generic parameters, there isn't any good way to support a type-based where. At the moment, the language team has no intentions of adding this feature. However, we are going to add a new method on Iterable to filter for specific types. A new function, of<T>() or ofType<T>, will allow developers to filter an Iterable and get a new Iterable of the requested type.