2017-09-08 @floitschG
Welcome to the Dart Language and Library Newsletter.
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); } }
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 dynamic
s 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}) { ... }
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.