OLD | NEW |
(Empty) | |
| 1 # Dart Language and Library Newsletter |
| 2 2017-09-01 |
| 3 @floitschG |
| 4 |
| 5 Welcome to the Dart Language and Library Newsletter. |
| 6 |
| 7 ## The Case Against Call |
| 8 Dart 1.x supports callable objects. By adding a `call` method to a class, instan
ces of this class can be invoked as if they were functions: |
| 9 |
| 10 ``` dart |
| 11 class Square { |
| 12 int call(int x) => x * x; |
| 13 toString() => "Function that squares its input"; |
| 14 } |
| 15 |
| 16 main() { |
| 17 var s = new Square(); |
| 18 print(s(4)); // => 16. |
| 19 print(s); // => Function that squares its input. |
| 20 print(s is int Function(int)); // => true. |
| 21 } |
| 22 ``` |
| 23 |
| 24 Note that `Square` doesn't need to implement any `Function` interface: as soon a
s there is a `call` method, all instances of the class can be used as if they we
re closures. |
| 25 |
| 26 While we generally like the this feature (let's be honest: it's pretty cool), th
e language team is trying to eventually remove it from the language. In this sec
tion, we explain the reasons for this decision. |
| 27 |
| 28 ### Wrong Name |
| 29 Despite referring to the feature as the "call operator", it is actually not impl
emented as an operator. Instead of writing the call operator similarly to other
operators (like plus, minus, ...), it's just a special method name. |
| 30 |
| 31 As an operator we would write the `Square` class from above as follows: |
| 32 ``` dart |
| 33 class Square { |
| 34 int operator() (int x) => x * x; |
| 35 } |
| 36 ``` |
| 37 |
| 38 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: |
| 39 |
| 40 ``` dart |
| 41 var s = new Square(); |
| 42 var f = s.call.call.call.call.call.call; |
| 43 print(f(3)); // => 9; |
| 44 ``` |
| 45 |
| 46 If the `call` operator was an actual operator, there wouldn't be any way to tear
off the operator itself. |
| 47 |
| 48 ### Tear-Offs are Too Good |
| 49 Tearing off a function is trivial in Dart. Simply referring to the corresponding
method member tears off the bound function: |
| 50 |
| 51 ``` dart |
| 52 class Square { |
| 53 int square(int x) => x * x; |
| 54 } |
| 55 |
| 56 main() { |
| 57 var s = new Square(); |
| 58 var f = s.square; |
| 59 print(f(3)); // => 9. |
| 60 } |
| 61 ``` |
| 62 |
| 63 The most obvious reason for a call-operator is to masquerade an instance as a fu
nction. 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`. |
| 64 |
| 65 The following contrived example shows how a program could use these properties. |
| 66 |
| 67 ``` dart |
| 68 // The `Element` class and the `disposedElements` getter are provided |
| 69 // by some framework. |
| 70 |
| 71 /// An element that reacts to mouse clicks. |
| 72 class Element { |
| 73 /// The element's click handler is a function that takes a `MouseEvent`. |
| 74 void Function(MouseEvent) clickCallback; |
| 75 } |
| 76 |
| 77 /// A stream that informs the user of elements that have been disposed. |
| 78 Stream<Element> disposedElements = ...; |
| 79 |
| 80 // ============= The following code corresponds to user code. ===== |
| 81 |
| 82 // Attaches a click handler to the element of the given name |
| 83 // and writes the clicks to a file. |
| 84 void logClicks(String name) { |
| 85 var sink = new File("$name.txt").openWrite(); |
| 86 var element = screen.getElement(name); |
| 87 element.clickCallback = sink.writeln; |
| 88 } |
| 89 |
| 90 main() { |
| 91 logClicks('demo'); |
| 92 logClicks('demo2'); |
| 93 disposedElements.listen((element) { |
| 94 // Would like to close the file for the registered handlers. |
| 95 // ------ |
| 96 }); |
| 97 } |
| 98 ``` |
| 99 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 kno
w how to find the `IOSink` that corresponds to the element that is removed. |
| 100 |
| 101 One easy solution is to add a global map that stores the mapping between the ele
ments and the opened sinks. Alternatively, we can introduce a callable class tha
t stores the open file: |
| 102 |
| 103 ``` dart |
| 104 // A class that connects the open output file with the handlers. |
| 105 class ClickHandler { |
| 106 final IOSink sink; |
| 107 ClickHandler(this.sink); |
| 108 void call(Object event) { |
| 109 sink.writeln(event); |
| 110 } |
| 111 } |
| 112 |
| 113 // Attaches a click handler to the element of the given name |
| 114 // and writes the clicks to a file. |
| 115 void logClicks(String name) { |
| 116 var sink = new File("$name.txt").openWrite(); |
| 117 var handler = new ClickHandler(sink); |
| 118 var element = screen.getElement(name); |
| 119 // Uses the callable object as handler. |
| 120 element.clickCallback = handler; |
| 121 } |
| 122 |
| 123 main() { |
| 124 logClicks('demo'); |
| 125 logClicks('demo2'); |
| 126 disposedElements.listen((element) { |
| 127 // ============ |
| 128 // Casts the function back to a `ClickHandler` class. |
| 129 var handler = element.clickCallback as ClickHandler; |
| 130 // Now we can close the sink. |
| 131 handler.sink.close(); |
| 132 }); |
| 133 } |
| 134 ``` |
| 135 |
| 136 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 prog
ram can retrieve the handler, cast it back to `ClickHandler` and read the `IOSin
k` out of it. |
| 137 |
| 138 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 proper
ties, please let us know. |
| 139 |
| 140 ### Typing |
| 141 A class that represents, at the same time, a nominal type and a structural funct
ion type tremendously complicates the type system. |
| 142 |
| 143 As a first example, let's observe a class that uses a generic type as parameter
type to its `call` method: |
| 144 |
| 145 ``` dart |
| 146 class A<T> { |
| 147 void call(T arg) {}; |
| 148 } |
| 149 |
| 150 main() { |
| 151 var a = new A<num>(); |
| 152 A<Object> a2 = a; // OK. |
| 153 void Function(int) f = a; // OK. |
| 154 // But: |
| 155 A<int> a3 = a; // Error. |
| 156 void Function(Object) f2 = a; // Error. |
| 157 } |
| 158 ``` |
| 159 |
| 160 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 sou
ndness.) |
| 161 |
| 162 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
. |
| 163 |
| 164 Note that the assignment to `a2` uses a supertype (`Object`) of `num` at the lef
t-hand side, whereas the assignment to `f` uses a subtype (`int`). We say that t
he assignment to `a2` is *covariant*, whereas the assignment to `f` is *contrava
riant* on the generic type argument. |
| 165 |
| 166 Our type system can handle these cases, and correctly inserts the necessary chec
ks to ensure soundness. However, it would be nice, if we didn't have to deal wit
h objects that are, effectively, bivariant. |
| 167 |
| 168 Things get even more complicated when we look at subtyping rules for `call` meth
ods. Take the following "simple" example: |
| 169 |
| 170 ``` dart |
| 171 class C { |
| 172 void call(void Function(C) callback) => callback(this); |
| 173 } |
| 174 |
| 175 main() { |
| 176 C c = new C(); |
| 177 c((_) => null); // <=== ok. |
| 178 c(c); // <=== ok? |
| 179 } |
| 180 ``` |
| 181 |
| 182 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` i
tself). |
| 183 |
| 184 The type system has to decide if an instance of type `C` (here `c`) is assignabl
e to the parameter type. For simplicity, we only focus on subtyping, which corre
sponds to the intuitive "Apple" can be assigned to "Fruit". Usually, subtyping i
s written using the "<:" operator: `Apple <: Fruit`. This notation will make thi
s text shorter (and *slightly* more formal). |
| 185 |
| 186 In our example, the type system thus wants to answer: `C <: void Function(C)`? S
ince `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 ca
n now compare these types structurally: `void Function(void Function(C)) <: void
Function(C)`? |
| 187 |
| 188 It starts by looking at the return types. In our case these are trivially assign
able: both are `void`. Next up are the parameter types: `void Function(C)` on th
e left, and `C` on the right. Since these types are in parameter position, we ha
ve to invert the operands. Formally, this inversion is due to the fact that argu
ment types are in contravariant position. Intuitively, it's easy to see that a f
unction that takes a *fruit function* (`Function(Fruit)`) can always be used in
places where an *apple function* (`Function(Apple)`) is required: `Function(Frui
t) <: Function(Apple)` because `Apple <: Fruit`. |
| 189 |
| 190 Getting back to our example, we had just concluded that the return types of `voi
d Function(void Function(C)) <: void Function(C)` matched and were looking at th
e parameter types. After switching sides we have to check whether `C <: void Fun
ction(C)`. |
| 191 |
| 192 If this looks familiar, you paid attention: this is the question we tried to ans
wer in the first placeā¦ |
| 193 |
| 194 Fundamentally, this means that Dart (with the `call` method) features recursive
types. Depending on the resolution algorithm of the type system we can now eithe
r conclude that: |
| 195 - `C <: void Function(C)`, if we use a co-inductive algorithm that tracks recurs
ion (which is just fancy wording for saying that we assume everything works and
try to see if things break), or |
| 196 - `C </: void Function(C)`, if we use an inductive algorithm that tracks recursi
on. (Start with nothing, and build up the truth). |
| 197 |
| 198 This is just one out of multiple issues that `call` methods bring to Dart's typi
ng system. Fortunately, we are not the first ones to solve these problems. Recur
sive 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. |
| 199 |
| 200 ### Conclusion |
| 201 Given all the complications the `call` method, the language team intends to even
tually remove this feature from the language. |
| 202 |
| 203 Our plan was to slowly phase `call` methods out over time, but we are now invest
igating, if we should take the jump with Dart 2.0, so that we can present a simp
ler type system for our Dart 2.0 specification. |
| 204 |
| 205 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. |
| 206 |
| 207 ## Limitations on Generic Types |
| 208 A common operation in Dart is to look through an iterable, and only keep objects
of a specific type. |
| 209 |
| 210 ``` dart |
| 211 class A {} |
| 212 class B extends A {} |
| 213 |
| 214 void main() { |
| 215 var itA = new Iterable<A>.generate(5, (i) => i.isEven ? new A() : new B()); |
| 216 var itB = itA.where((x) => x is B); |
| 217 } |
| 218 ``` |
| 219 In this example, `itA` is an `Iterable` that contains both `A`s and `B`s. The `w
here` method then filters these elements and returns an `Iterable` that just con
tains `B`s. It would thus be great to be able to use the returned `Iterable` as
an `Iterable<B>`. Unfortunately, that's not the case: |
| 220 ``` dart |
| 221 print(itB is Iterable<B>); // => false. |
| 222 print(itB.runtimeType); // => Iterable<A>. |
| 223 ``` |
| 224 The dynamic type of `itB` is still `Iterable<A>`. This becomes obvious, when loo
king at the signature of `where`: `Iterable<E> where(bool test(E element))` (whe
re `E` is the generic type of the receiver `Iterable`). |
| 225 |
| 226 It's natural to wonder if we could improve the `where` function and allow the us
er to provide a generic type when they want to: `itA.where<B>((x) => x is B)`. I
f the user provides a type, then the returned iterable should have that generic
type. Otherwise, the original type should be used: |
| 227 |
| 228 ``` dart |
| 229 // We would like the following return types: |
| 230 var anotherItA = itA.where(randomBool); // an Iterable<A>. |
| 231 var itB = itA.where<B>((x) => x is B); // an Iterable<B>. |
| 232 ``` |
| 233 |
| 234 The signature of `where` would need to look somehow similar to: |
| 235 ``` dart |
| 236 Iterable<T> where<T>(bool test(E element)); |
| 237 ``` |
| 238 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 t
ype with `dynamic`. So, `anotherItA` would just be an `Iterable<dynamic>` and no
t `Iterable<A>`. |
| 239 |
| 240 The only way to provide "default" values for generics is to use the `extends` cl
ause such as: |
| 241 ``` dart |
| 242 Iterable<T> where<T extends E>(bool test(E element)); |
| 243 ``` |
| 244 This is because Dart's type inference uses the bound of a generic type when no g
eneric argument is provided. |
| 245 |
| 246 Running our tests, this looks promising: |
| 247 ``` dart |
| 248 var anotherItA = itA.where(randomBool); |
| 249 print(anotherItA.runtimeType); // => Iterable<A>. |
| 250 |
| 251 var itB = itA.where<B>((x) => x is B); |
| 252 print(itB.runtimeType); // => Iterable<B>. |
| 253 ``` |
| 254 |
| 255 Clearly, given the title of this section, there must be a catch... |
| 256 |
| 257 While our simple examples work, adding this generic type breaks down with covari
ant generics (`List<Apple>` is a `List<Fruit>`). Let's try our new `where` funct
ion on a more sophisticated example: |
| 258 |
| 259 ``` dart |
| 260 int nonNullLength(Iterable<Object> objects) { |
| 261 return objects.where((x) => x != null).length; |
| 262 } |
| 263 |
| 264 var list = [1, 2]; // a List<int>. |
| 265 print(nonNullLength(list)); |
| 266 ``` |
| 267 |
| 268 The `nonNullLength` function just filters out all elements that are `null` and r
eturns 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. |
| 269 |
| 270 The `where` in `nonNullLength` has no generic argument, and the type inference h
as to fill it in. Without any provided generic argument and no contextual inform
ation, the type inference uses the bound of the generic parameter. For our impro
ved `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`. |
| 271 |
| 272 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 repor
t an error. In Dart 2.0 the `nonNullLength` function would therefore throw. |
| 273 |
| 274 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 implement
ed in a very recent DDC.) Eventually, all our tools will implement the required
checks. |
| 275 |
| 276 Without actual default values for generic parameters, there isn't any good way t
o support a type-based `where`. At the moment, the language team has no intentio
ns of adding this feature. However, we are going to add a new method on `Iterabl
e` 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 request
ed type. |
OLD | NEW |