2017-08-18 @floitschG
Welcome to the Dart Language and Library Newsletter.
Did you know that you can write trailing commas to arguments and parameters? This feature was added to the specification about a year ago.
It's main use-case is to unify parameter and argument lists that span multiple lines. For example, Flutter uses it extensively to keep tree-like instantiations nicely aligned:
return new Material( // Column is a vertical, linear layout. child: new Column( children: <Widget>[ new MyAppBar( title: new Text( 'Example title', style: Theme.of(context).primaryTextTheme.title, ), ), new Expanded( child: new Center( child: new Text('Hello, world!'), ), ), ], ),
Note how every argument list ends with a comma. The dartfmt
tool knows about these trailing commas and ensures that the individual entries stay on their own lines so that it is easy to move them around with cut and paste.
Recently, we also updated the specification to allow trailing commas in assert
s. This makes the syntax of assert
s more consistent with function calls.
A few months ago, we added a new function type syntax to Dart (we mentioned it in our first newsletter).
// Examples: typedef F = void Function(int); // A void function that takes an int. void foo(T Function<T>(T x) f) { // foo takes a generic function. ... } class A { // A has a field `f` of type function that takes a String and returns void. void Function(String) f; }
Before we added the new function-type syntaxes, we evaluated multiple options. In this section I will summarize some of the discussions we had.
The new function-type syntax intends to solve three issues:
typedef
syntax doesn't support generic types.typedef f(int);
is not a typedef for a function that expects an int
, but for a function that expects dynamic
and names the argument “int”.With Dart 2.0 we will support generic methods, and also generic closures. This means that a function can accept a generic function as argument. We were lacking a way to express this type.
Dart 1.x has two ways to express function types: a) an inline syntax for parameters and b) typedef
s.
It was easy to extend the inline syntax to support generic arguments:
// Takes a function `factoryForA` that is generic on T. void foo(A<T> factoryForA<T>()) { A<int> x = factoryForA<int>(); A<String> x = factoryForA<String>(); }
However, there was no easy way to do the same for typedef
s:
typedef A<T> FactoryForA<T>();// Does *not* do what we want it to do: FactoryForA f; // A function that returns an `A<dynamic>`. FactoryForA<String> f2; // A function that returns an `A<String>`. f<int>(); // Error: `f` is not generic.
We had already used the most consistent place for the generic method argument as a template argument to the typedef
itself. If we could go back in time, we could change it as follows:
typedef<T> List<T> TemplateTypedef(); TemplateTypedef<int> f; // A function that returns a List<int>. TemplateTypedef f; // A function that returns a List<dynamic>. typedef List<T> GenericTypedef<T>(); GenericTypedef f; // A function that is generic. List<int> ints = f<int>(); List<String> strings = f<String>();
Given that this would be a breaking change we explored alternatives that would also solve the other two issues. In particular the new syntax had to work for locals and fields, too.
First and foremost the new syntax had to be readable. It also had to solve the three mentioned issues. Finally, we wanted to make sure, we didn't choose a syntax that would hinder future evolution of the language. We made sure that the syntax would work with:
(int)->int?; // A function that is nullable, or that returns a nullable integer? Problem disappears with <- int <- (int)? ; vs int? <- (int)
For all the following proposals we had decided that the arguments could either be just the type, or optionally have a name. For example, (int)->int
is equivalent to (int id)->int
. Especially with multiple arguments of the same type, providing a name can make it much easier to reason about the type: (int id, int priority) -> void
. However, type-wise these parameter names are ignored.
All of the proposals thus interpret single-argument identifiers as types. This is in contrast to the old syntax where a single identifier would state the name of the parameter: in void foo(bar(int)) {...}
the int
is the name of the parameter to bar
. This discrepancy is hopefully temporary, as we intend to eventually change the behavior of the old syntax.
Using ->
as function-type syntax feels very natural and is used in many other languages: Swift, F#, SML, OCaml, Haskell, Miranda, Coq, Kotlin, and Scala (with =>).
Examples:
typedef F = (int) -> void; // Function from (int) to void. typedef F<T> = () -> List<T>; // Template Typedef. typedef F = <T>(T) -> List<T>; // Generic function from T to List<T>.
We could even allow a short form when there is only one argument: int->int
.
We have experimented with this syntax: [https://codereview.chromium.org/2439573003/]
Advantages:
int->int
.Open questions:
(int, int) -> String
or (int, int)->String
, etc.Disadvantages:
typedef F = (int) -> int?; // Nullable function or nullable int? // Could be disambiguated as follows: typedef F = ((int)->int)?; // Clearly nullable function.
This section explores using <-
as function-type syntax. There is at least one other language that uses this syntax: Twelf.
Examples:
typedef F = void <- (int); // Function from (int) to void. typedef F<T> = List<T> <- (); // Template Typedef. typedef F = List<T> <- <T>(T); // Generic function from T to List<T>.
Could also allow a short form: int<-int
. (For some reason this seems to read less nicely than int->int
.)
We have experimented with this syntax: [https://codereview.chromium.org/2466393002/]
Advantages:
typedef
s, where the return value is more likely to stay on the first line.<-
doesn't require a lot of look-ahead.->
.typedef F = int <- (int)?; // Nullable function. typedef F = int? <- (int); // Returns nullable int.
Open Questions:
Disadvantages:
<-
is ambiguous: x<-y ? foo(x) : foo(y) // if x < (-y) ...
.->
.Dart already uses Function
as general type for functions. It is relatively straightforward to extend the use of Function
to include return and parameter types. (And no: it‘s not Function<int, int>
since that wouldn’t work for named arguments).
typedef F = void Function(int); // Function from (int) to void. typedef F<T> = List<T> Function(); // Template Typedef. typedef F = List<T> Function<T>(T); // Generic function from T to List<T>.
This form does not allow any shorthand syntax, but fits nicely into the existing parameter syntax.
Before we accepted this syntax, we had experimented with this syntax: [https://codereview.chromium.org/2482923002/]
Advantages:
Function
:Function f; // a function. Function(int x) f; // a function that takes an int. double Function(int) f; // a function that takes an int and returns a double.
Disadvantages:
We found that the Function
-based syntax fits nicely into Dart and fulfills all of our requirements. Due to its similarity to function declarations it is also very future-proof. Any feature that works with function declarations should work with the Function
-type syntax, as well.