Dart Language and Library Newsletter

2017-09-22 @floitschG

Welcome to the Dart Language and Library Newsletter.

Did You Know?

Literal Strings

Dart has multiple ways to write literal strings. This section shows each of them and discusses when they are useful.

Quoted Strings

For the lack of a better word I call the standard strings “quoted strings”. These are normal strings delimited by single or double quotes. There is no difference between those two delimiters.

var x = "this is a string";
var y = "this is another string";

Quoted strings can contain escape sequences (see below) and string-interpolations. A dollar followed by an identifier inserts the toString() of the referenced value:

var hello = "Hello";
var readers = "readers";
print("$hello $readers");

It is common to have no space between two spliced values. For this reason, the specification actually doesn't allow any identifier after the dollar, but requires the identifier to be without any dollar.

var str1 = "without";
var str2 = "space";
print("$str1$str2");  // => withoutspace.

var dollar$variable = "needs more work";
print("$dollar$variable");  // Error. This won't work.

For identifiers that contain dollars, or for more complex expressions, one can use curly braces to delimit the expression that should be spliced in.

var dollar$variable = "now works";
print("${dollar$variable}");  // => now works.

var x = 1;
var y = 2;
print("${x + y}");  // => 3.

Quoted strings are single line strings that may not contain a verbatim newline character.

// ERROR: missing closing ".
var notOk = "Not possible
  to write strings over
  multiple lines.";

This is a safety feature: if all strings can contain newlines, it's too easy for a missing end-quote to trip up the user and the compiler. In the worst case the program might even still be valid but just do something completely different.

var someString = 'some string";
var secondString = 'another one";

Dart has requires developers to opt-in to multiline strings (see below), or to use escape sequences to insert newline characters without actually breaking the code line.

Escapes

Dart features the following common escape sequences:

  • \n: a newline. (0x0A in the ASCII table).
  • \r: carriage return (0x0D).
  • \f: form feed (0x0C).
  • \b: backspace (0x08).
  • \t: tab (0x09).
  • \v: vertical tab (0x0B).
  • \x D0 D1, where D0 and D1 are hex digits: the codeunit designated by the hexadecimal value.

For Unicode Dart furthermore supports the following sequences:

  • `\u{hex-digits}: the unicode scalar value represented by the given hexadecimal value.
  • \u D0 D1 D2 D3 where D0 to D3 are hex digits: equivalent to \u{D0 D1 D2 D3}.

Every other \ escape represents the escaped character. It can removed in all cases, except for \\ and the delimiters of the string.

var escaping = "One can escape with \"\\\".";
print(escaping);  // => One can escape with "\".

Raw Strings

A raw string is a string that ignores escape sequences. It is written by prefixing the string literal with a 'r'.

var raw = r'\no\escape\';
print(raw);  // => \no\escape\

Raw strings don't feature string interpolations either. In fact, a raw string is a convenient way to write the dollar string: r"$".

Raw strings are particularly useful for regular expressions.

Multiline Strings

A string that is delimited by three single or double quotes is a multiline string. Unlike to the other strings, it may span multiple lines:

var multi = """this string
spans multiple lines""";

To make it easier to align the contents of multiline strings, an immediate newline after the starting delimiters is ignored:

var multi = """
this string
spans multiple lines""";

print(multi.startsWith("t"));  // => true.

Contrary to Ceylon (https://ceylon-lang.org/documentation/1.3/reference/literal/string/) Dart does not remove leading indentation. In fact, Dart currently has no builtin way to remove indentation of multiline strings at all. We plan to add this functionality in the future: https://github.com/dart-lang/sdk/issues/30864.

Multiline strings can contain escape sequences and string interpolations. They can also be raw.

var interpolation = "Interpolation";
var multiEscapeInter = """
  $interpolation works.
  escape sequences as well: \u{1F44D}""";

var raw = r"""
  a raw string doesn't
  use $ for interpolation.
""";

Multiline strings are not just useful for strings that span multiple lines, but also when both kind of quotes happen to be in the text. This is especially the case for raw strings since they can't escape the delimiters.

var singleLine = '''The book's title was "in quotes"''';
var rawSingleLine = r"""contains "everything" $and and the 'kitchen \sink'""";

Concatenated Strings

Whenever two string literals are written next to each other, they are concatenated. This is most often used when a string exceeds the recommended line length, or when different parts of a string literal are more easy to write with different literals.

var interpolation = "interpolation";
var combi = "a string "
   'that goes over multiple lines '
   "and uses $interpolation "
   r"and \raw strings"

Void as a Type

As mentioned in earlier newsletters, we want to make void more useful in Dart. In Dart 1.24 we made the first small improvements, but the big one is to allow void as a type annotation and generic type argument (as in Future<void>).

The following text should be considered informative and sometimes ignores minor nuances when it simplifies the discussion.

Motivation

Dart currently allows void only as a return type. This covers the most common use-case, but, more and more, we find that void would be very useful in other places. In particular, in asynchronous programming and as the result of a type-inference, allowing void in more locations would make a lot of code simpler. In both cases, a return type directly feeds into a generic type.

In asynchronous programming, most often with async/await, the result of a computation is wrapped in a Future. When the computation has a return-type void, then we would like to see void as the generic type of Future. Since that's not yet allowed, many developers use Null, which comes with other problems (already highlighted in previous newsletters).

The type inference already uses the return types of methods to infer generic arguments to functions. The void type, as a generic argument, thus already exists in strong mode. Users just don't have a way to write it.

void bar() {}

main() {
  var f = new Future.value(bar());
  print(f.runtimeType);  // => _Future<void>.

History

Historically, void was (and still is) very restricted in Dart. The Dart 1.x specification makes void a reserved word that can only appear as a return type for functions. This means that implementations don't ever need to reify the void type itself, and, indeed, the dart2js implementation simply implemented void as a boolean bit on function types.

Conceptually, void serves as a way to distinguish procedures (no return value) and functions (return value). The void type is not part of the type hierarchy as can best be seen when looking at Dart's function-type rules. Dart 1.x has very relaxed function-subtyping rules, that is (roughly speaking) bivariant on the return type and the parameter types. For example, Object Function(int) is a subtype of int Function(Object). Despite these liberties, a void Function() is not an int Function().

Example:

// In Dart 1.x
Object foo(int x) => x;
void bar() {}

main() {
  print(foo is int Function(Object)); // true
  print(bar is Object Function()); // false
}

At the same time, void functions are not really procedures, either. Like every function, void functions do return a value (usually null), and void methods can be overridden with non-void methods.

class A {
  void foo() {};
  void bar() => foo();
}

class B extends A {
  int foo() => 499;
}

Given these properties, a better interpretation of void is to think of them as values that simply shouldn‘t be used. Having the return type void is another way of saying: “I return something of type Object, but the value is useless and should not be relied upon”. It doesn’t say anything about subclasses which might have a more interesting value and are free to change void to something else.

With this interpretation, making void a full-fledged type becomes straight-forward: treat void similarly to Object (subtype-wise), but disallow using values of static type void.

The void Type

Now that the meaning of void is clear, it's just a matter of adding it to the language.

Over long discussions we came up with a plan to land this feature in two steps:

  1. generalize the void type and disallow obvious misuses,
  2. add more checks to disallow less obvious accesses to void.

Step 1

We start by syntactically allowing void everywhere that other types are allowed. Until now, Dart prohibited uses of void except as return types of functions.

When void is used as a type, the subtyping rules treat it similar to Object. This means that Future<void> can be used everywhere, where Future<Object> is allowed. The only exception is function types, where the current restrictions are kept: a void Function() is still not an Object Function().

A simple restriction disallows obvious misuses of void: expressions of static type void are only allowed in specific productions. Among many other locations, they are not allowed as right-hand sides of assignments or as arguments to function calls:

int foo(void x) {  // `foo` receives a `void`-typed argument.
  var y = x;  // Disallowed.
  bar(x);  // Disallowed.
}

Future<void> f = new Future(() { /* do something. */ });
f.then((x) {  // Type inference infers `x` of type `void`.
  print(x);  // Disallowed.
});

var list = Future.await([f]);  // `f` from above. `list` is of type `List<void>`.
print(list[0]);  // Disallowed.

Together with a type-inference that recognizes (and infers) void, these changes make void a full type in Dart. As discussed in the next section there are static checks that we want to add, but even in this form, void brings lots of benefits.

A common pattern is to use Null for generic types that would have been void if it was allowed. This might seem intuitive, since void functions effectively return null when there is no return statement, but it also prevents us from providing users with some useful warnings. Future<Null> is a subtype of every other Future. There is no static or dynamic error when a Future<Null> is assigned to Future<int>. With Future<void> there would be a dynamic error when it is assigned to Future<int>.

Furthermore, using a void value (more concretely an expression of static type void) would be forbidden in most syntactic locations. Examples:

voidFuture.then((x) { ... use(x) ... });  // x is inferred as `void`, and the use is an error.
var x = await voidFuture;  // Error.

var y = print("foo");  // Error. void expression in RHS of an assignment.

foo(void x) {}
// Note that the check is syntactic and doesn't look at the target type:
foo(print("bar"));  // Error. void expression in argument position.

These errors are only checked statically (since, at runtime, no value can be of type void).

Step 2

In step 1 we are restricting obvious uses of void. There remain many holes how a void value can leak. The easiest is to use the void-Object equivalence when void is used as a generic type:

Future<void> f1 = new Future(() { /* do something */ });
Future<Object> f2 = f1;  // Legal, since Object is treated like void.
f2.then(print);  // Uses the value.

In step 2 these non-obvious void uses are clamped down. It's important to note that none of our proposals makes it impossible to leak void. Voidness preservation is intended to provide reasonable warnings and errors while not getting too much in the way.

Take, for example, the following class that uses generic types:

class A<T> {
  final T x;
  A(T Function() f) : this.x = f();
  toString() => "A($x)";
}

Since it uses x.toString() in the toString method, it uses x as an Object. That means that new A(voidFun).toString() ends up using x which was marked as void.

In the remainder of this document we use the term “void value” to refer to variables like A.x. That is, values that users should not access, because they are or were typed as void (directly or indirectly).

One could easily argue that this is a limitation of the type system. Either A should opt into supporting T being equal to void, or, inversely, it should require T to extend Object.

class A<T extends void> { ... }
// or
class A<T extends Object> { ... }

In practice, this would be too painful. A surprising number of classes actually need to support void values. For example List<void> is very useful for Future.wait which may take a List<Future<void>> and returns the result of the futures.

At the same time, List also demonstrates that restricting access to its generic values gets in the way. List has a join method that combines the strings of all entries, by invoking toString() on all of its entries. This means, that the generic type of List must both support void and yet be usable as Object.

For all these reasons, Dart doesn‘t guarantee that void values can never be accessed. In step 2, Dart just gets static rules that disallow assignments that obviously leak void values and that could be avoided. The easiest example is List<Object> x = List<void>. A rule will forbid such assignments. This fits the developers intentions: "a list of values that shouldn’t be used, can't be assigned to a list that wants to use the values".

The exact rules for voidness preservations are complicated and haven‘t been finalized yet. They are surprisingly difficult to specify, especially because we want to match them with user’s intuitions. We will add them at a later point. Initially, they will be warnings, and eventually, we will make them part of the language and make them errors.

It makes sense to have step 1 separately, even though it is only checking direct usage of void values: The execution of a Dart program will proceed just fine even in the case where a “void value” is passed on and used. It is going to be a regular Dart object, no fundamental invariants of the runtime are violated, and it is purely an expression of programmer intent that this particular value should be ignored. This makes it possible to get started using void to indicate that certain values should be ignored when used in simple and direct ways. Later, with step 2 checks in place, some indirect usages can be flagged as well.

Note that we do not intend to ever add runtime checking of voidness preservation, void will always work like Object at runtime; at least, once void function return types aren't treated specially anymore. As mentioned earlier, Dart treats void functions as separate from non-void functions (the “procedure” / “function” distinction). Once voidness preservation rules are in place, we intent to remove that special treatment. Static checks will prevent the use of a void function in a context where a non-void function is expected. This means that the static checks will catch almost all misuses of void functions. The special handling of functions in the dynamic typing system would then become unnecessary.