Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(663)

Side by Side Diff: docs/newsletter/20170901.md

Issue 3012723002: Newsletter 2017-09-01. (Closed)
Patch Set: Created 3 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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.
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698