Dart Language and Library Newsletter

2017-08-18 @floitschG

Welcome to the Dart Language and Library Newsletter.

If you missed it

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 asserts. This makes the syntax of asserts more consistent with function calls.

Function Type Syntax

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.

Motivation

The new function-type syntax intends to solve three issues:

  1. the old typedef syntax doesn't support generic types.
  2. the old function syntax can't be used for fields and locals.
  3. in the old syntax, providing only one identifier in an argument position is interpreted as name and not type. For example: 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) typedefs.

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 typedefs:

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:

  • nullability: the syntax must be nullable without too much hassle:
     (int)->int?;  // A function that is nullable, or that returns a nullable integer?
     Problem disappears with <-
     int <- (int)? ; vs int? <- (int) 
    
  • union types (in case we ever want them).

Common Characteristics

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.

Right -> Arrow

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:

  • easy to learn and familiar to many developers.
  • could support shorthand form int->int.

Open questions:

  • support shorthand form?
  • whitespace. Should it be (int, int) -> String or (int, int)->String, etc.

Disadvantages:

  • Relatively late token. The parser would have to do relatively big lookaheads.
  • Works badly with nullable types:
    typedef F = (int) -> int?;  // Nullable function or nullable int?
    // Could be disambiguated as follows:
    typedef F = ((int)->int)?;   // Clearly nullable function.
    
Left <- Arrow

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:

  • return value is on the left, similar to normal function signatures. This also simplifies typedefs, where the return value is more likely to stay on the first line.
  • faster to parse, since the <- doesn't require a lot of look-ahead.
  • relatively similar to ->.
  • no problems with nullable types:
    typedef F = int <- (int)?;  // Nullable function.
    typedef F = int? <- (int);  // Returns nullable int.
    

Open Questions:

  • whitespace?
  • support shorthand form?

Disadvantages:

  • <- is ambiguous: x<-y ? foo(x) : foo(y) // if x < (-y) ....
  • Not as familiar as ->.
Function

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:

  • very similar to the syntax of the corresponding function declarations.
  • no ambiguity.
  • (almost) no new syntax. That is, the type can be immediately extrapolated from other syntax.
  • no open questions wrt whitespace.
  • symmetries with existing use of 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:

  • longer.
Conclusion

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.