8000 Merge pull request #213 from phaller/topic/sip-async · kenbot/scala.github.com@9802cbf · GitHub
[go: up one dir, main page]

Skip to content

Commit 9802cbf

Browse files
committed
Merge pull request scala#213 from phaller/topic/sip-async
Add Async SIP
2 parents e243c2e + 72fa684 commit 9802cbf

File tree

1 file changed

+298
-0
lines changed

1 file changed

+298
-0
lines changed
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
---
2+
layout: sip
3+
disqus: true
4+
title: SIP-22 - Async
5+
---
6+
7+
**By: Philipp Haller and Jason Zaugg**
8+
9+
## Introduction
10+
11+
This is a proposal to add constructs that simplify asynchronous and concurrent programming in Scala. The main constructs, async and await, are inspired by similar constructs introduced in C# 5.0. The main purpose of async/await is to make it possible to express efficient asynchronous code in a familiar direct style (where suspending operations look as if they were blocking). As a result, non-blocking code using Scala’s futures API \[[1][1]\] can be expressed without using higher-order functions, such as map and flatMap, or low-level callbacks.
12+
13+
On the level of types, async and await are methods with simple, intuitive types:
14+
15+
def async[T](body: => T): Future[T]
16+
def await[T](future: Future[T]): T
17+
18+
Here, `Future[T]` refers to the `Future` trait in package `scala.concurrent`. (The system can be adapted to other implementations of future-like abstractions; at the moment the API with the required extension points is internal, though.) The above methods are used as follows:
19+
20+
val fut = async {
21+
slowComputation()
22+
}
23+
24+
The async construct marks a block of asynchronous code, and returns a future. Depending on the execution context in the implicit scope (see \[[1][1]\]), the block of asynchronous code is either executed on the current thread or in a thread pool. The async block can contain calls to await:
25+
26+
val futureDOY: Future[Response] =
27+
WS.url("http://api.day-of-year/today").get
28+
29+
val futureDaysLeft: Future[Response] =
30+
WS.url("http://api.days-left/today").get
31+
32+
val respFut = async {
33+
val dayOfYear = await(futureDOY).body
34+
val daysLeft = await(futureDaysLeft).body
35+
Ok(s"$dayOfYear: $daysLeft days left!")
36+
}
37+
38+
Line 1 and 4 define two futures obtained as results of asynchronous requests to two hypothetical web services using an API inspired by Play Framework \[[2][2]\] (for the purpose of this example, the definition of type `Response` is unimportant). The `await` on line 8 causes the execution of the `async` block to suspend until `futureDOY` is completed (with a successful result or with an exception). When the future is completed successfully, its result is bound to the `dayOfYear` val, and the execution of the `async` block is resumed. When the future is completed with an exception (for example, because of a timeout), the invocation of `await` re-throws the exception that the future was completed with. In turn, this completes future respFut with the same exception. Likewise, the `await` on line 9 suspends the execution of the `async` block until futureDaysLeft is completed.
39+
40+
## Comparison with Scala’s Futures API
41+
42+
The provided async and await constructs can significantly simplify code coordinating multiple futures. Consider the following example, written using Scala’s futures API together with for-comprehensions:
43+
44+
def nameOfMonth(num: Int): Future[String] = ...
45+
val date = “““(\d+)/(\d+)“““.r
46+
47+
for { doyResponse <- futureDOY
48+
dayOfYear = doyResponse.body
49+
response <- dayOfYear match {
50+
case date(month, day) =>
51+
for (name <- nameOfMonth(month.toInt))
52+
yield Ok(s“It’s $name!“)
53+
case _ =>
54+
Future.successful(NotFound(“Not a...“))
55+
}
56+
} yield response
57+
58+
Line 1 defines an asynchronous method that converts an integer representing the number of a month to the name of the month (for example, the integer 2 is converted to "February"). Since the method is asynchronous, it returns a `Future[String]`. Line 2 defines a regular expression used to extract the month from a date string such as "07/24". The for-comprehension starting on line 4 first awaits the result of `futureDOY` (the example re-uses the definition of `futureDOY` shown earlier). Scala's futures provide methods like `map` and `flatMap`, and can thus be used as generators in for-comprehensions (for a more in-depth introduction of this feature see the official documentation \[[1][1]\]). The use of for-comprehensions can help make future-based code more clear, but in many cases it requires a significant amount of unnatural clutter and workarounds. The above example suffers from the following issues:
59+
60+
- To extract `dayOfYear`, we are forced to introduce the name `doyResponse`, a useless intermediate result (line 4);
61+
- to await the completion of the future returned by `nameOfMonth`, we are forced to use a nested for-comprehension (line 8);
62+
- the nested for-comprehension forces us to bind the result of nameOfMonth to name, a useless intermediate variable (line 8);
63+
- the nested for-comprehension forces us to introduce an artificial future that's completed upon creation (line 11);
64+
- the artificial future introduces additional overhead and garbage (line 11);
65+
- finally, the use of for-yield might obscure the actual domain which is asynchronous computations with non-blocking awaits.
66+
67+
The same example can be written using async/await as follows:
68+
69+
async {
70+
await(futureDOY).body match {
71+
case date(month, day) =>
72+
Ok(s“It’s ${await(nameOfMonth(month.toInt))}!“)
73+
case _ =>
74+
NotFound(“Not a date, mate!“)
75+
}
76+
}
77+
78+
This version avoids all drawbacks of the previous version listed above. In addition, the generated code is more efficient, because it creates fewer closures.
79+
80+
## Illegal Uses
81+
82+
The following uses of await are illegal and are reported as errors:
83+
- await requires a directly-enclosing async; this means await must not be used inside a closure nested within an async block, or inside a nested object, trait, or class.
84+
- await must not be used inside an expression passed as an argument to a by-name parameter.
85+
- await must not be used inside a Boolean short-circuit argument.
86+
- return expressions are illegal inside an async block.
87+
88+
## Implementation
89+
90+
We have implemented the present proposal using the macro system which has been introduced in Scala 2.10 as an experimental feature. Our implementation \[[3][3]\] is targeted at Scala 2.11.0, but runs on using Scala 2.10.1 without any limitations.
91+
92+
## Async Transform Specification
93+
94+
In the following we consider the transformation of an invocation `async { <block> }` of the async macro.
95+
Before the block of code (`<block>`) is transformed, it is normalized into a form amenable to a transformation into a state machine. This form is called the "A-Normal Form" (ANF), and roughly means that:
96+
97+
- `if`, `match`, and other control-flow constructs are only used as statements; they cannot be used as expressions;
98+
- calls to `await` are not allowed in compound expressions.
99+
100+
After the ANF transform, the async macro prepares the state machine
101+
transformation by identifying vals, vars and defs that are accessed
102+
from multiple states. These will be lifted out to fields in the state
103+
machine object.
104+
105+
The next step of the transformation breaks the code into "chunks."
106+
Each chunk contains a linear sequence of statements that concludes
107+
with a branching decision, or with the registration of a subsequent
108+
state handler as the continuation (the "on-completion handler"). Once
109+
all chunks have been built, the macro synthesizes a class representing
110+
the state machine. The class contains:
111+
112+
- an integer representing the current state ID
113+
- the lifted definitions
114+
- an `apply(value: Try[Any]): Unit` method that will be called on completion of each future. The behavior of this method is determined by the current state. It records the downcast result of the future in a field, and calls the `resume()` method.
115+
- the `resume(): Unit` method that switches on the current state and runs the users code for one "chunk," and either: (a) registers the state machine as the handler for the next future, or (b) completes the result promise of the async block, if at the terminal state.
116+
- an `apply(): Unit` method that starts the evaluation of the async block's body.
117+
118+
### Example
119+
120+
val future = async {
121+
val f1 = async { true }
122+
val x = 1
123+
def inc(t: Int) = t + x
124+
val t = 0
125+
val f2 = async { 42 }
126+
if (await(f1)) await(f2) else { val z = 1; inc(t + z) }
127+
}
128+
129+
After the ANF transform:
130+
- `await` calls are moved to only appear on the RHS of a value definition;
131+
- `if` is no longer used as an expression; instead each branch writes its result to a synthetic var;
132+
- the `ExecutionContext` used to run the async block is obtained as an implicit argument.
133+
134+
Follows the end result of the ANF transform (with very minor
135+
simplifications).
136+
137+
{
138+
();
139+
val f1: scala.concurrent.Future[Boolean] = {
140+
scala.concurrent.Future.apply[Boolean](true)(scala.concurrent.ExecutionContext.Implicits.global)
141+
};
142+
val x: Int = 1;
143+
def inc(t: Int): Int = t.+(x);
144+
val t: Int = 0;
145+
val f2: scala.concurrent.Future[Int] = {
146+
scala.concurrent.Future.apply[Int](42)(scala.concurrent.ExecutionContext.Implicits.global)
147+
};
148+
val await$1: Boolean = scala.async.Async.await[Boolean](f1);
149+
var ifres$1: Int = 0;
150+
if (await$1)
151+
{
152+
val await$2: Int = scala.async.Async.await[Int](f2);
153+
ifres$1 = await$2
154+
}
155+
else
156+
{
157+
ifres$1 = {
158+
val z: Int = 1;
159+
inc(t.+(z))
160+
}
161+
};
162+
ifres$1
163+
}
164+
165+
After the full async transform:
166+
167+
- one class is synthesized that represents the state machine. Its `apply()` method is used to start the computation (even the code before the first await call is executed asynchronously), and the `apply(tr: scala.util.Try[Any])` method will continue after each completed future that the async block awaits;
168+
169+
- each chunk of code is moved into the a branch of the pattern match in `resume$async`;
170+
171+
- value and function definitions accessed from multiple states are lifted to be members of class `stateMachine`; others remain local, e.g. `val z`;
172+
173+
- `result$async` holds the promise which is completed with the result of the async block;
174+
175+
- `execContext$async` holds the `ExecutionContext` that has been inferred.
176+
177+
Follows the end result of the full async transform (with very minor
178+
simplifications).
179+
180+
{
181+
class stateMachine$7 extends StateMachine[scala.concurrent.Promise[Int], scala.concurrent.ExecutionContext] {
182+
var state$async: Int = 0;
183+
val result$async: scala.concurrent.Promise[Int] = scala.concurrent.Promise.apply[Int]();
184+
val execContext$async = scala.concurrent.ExecutionContext.Implicits.global;
185+
var x$1: Int = 0;
186+
def inc$1(t: Int): Int = t.$plus(x$1);
187+
var t$1: Int = 0;
188+
var f2$1: scala.concurrent.Future[Int] = null;
189+
var await$1: Boolean = false;
190+
var ifres$1: Int = 0;
191+
var await$2: Int = 0;
192+
def resume$async(): Unit = try {
193+
state$async match {
194+
case 0 => {
195+
();
196+
val f1 = {
197+
scala.concurrent.Future.apply[Boolean](true)(scala.concurrent.ExecutionContext.Implicits.global)
198+
};
199+
x$1 = 1;
200+
t$1 = 0;
201+
f2$1 = {
202+
scala.concurrent.Future.apply[Int](42)(scala.concurrent.ExecutionContext.Implicits.global)
203+
};
204+
f1.onComplete(this)(execContext$async)
205+
}
206+
case 1 => {
207+
ifres$1 = 0;
208+
if (await$1)
209+
{
210+
state$async = 2;
211+
resume$async()
212+
}
213+
else
214+
{
215+
state$async = 3;
216+
resume$async()
217+
}
218+
}
219+
case 2 => {
220+
f2$1.onComplete(this)(execContext$async);
221+
()
222+
}
223+
case 5 => {
224+
ifres$1 = await$2;
225+
state$async = 4;
226+
resume$async()
227+
}
228+
case 3 => {
229+
ifres$1 = {
230+
val z = 1;
231+
inc$1(t$1.$plus(z))
232+
};
233+
state$async = 4;
234+
resume$async()
235+
}
236+
case 4 => {
237+
result$async.complete(scala.util.Success.apply(ifres$1));
238+
()
239+
}
240+
}
241+
} catch {
242+
case NonFatal((tr @ _)) => {
243+
{
244+
result$async.complete(scala.util.Failure.apply(tr));
245+
()
246+
};
247+
()
248+
}
249+
};
250+
def apply(tr: scala.util.Try[Any]): Unit = state$async match {
251+
case 0 => {
252+
if (tr.isFailure)
253+
{
254+
result$async.complete(tr.asInstanceOf[scala.util.Try[Int]]);
255+
()
256+
}
257+
else
258+
{
259+
await$1 = tr.get.asInstanceOf[Boolean];
260+
state$async = 1;
261+
resume$async()
262+
};
263+
()
264+
}
265+
case 2 => {
266+
if (tr.isFailure)
267+
{
268+
result$async.complete(tr.asInstanceOf[scala.util.Try[Int]]);
269+
()
270+
}
271+
else
272+
{
273+
await$2 = tr.get.asInstanceOf[Int];
274+
state$async = 5;
275+
resume$async()
276+
};
277+
()
278+
}
279+
};
280+
def apply: Unit = resume$async()
281+
};
282+
val stateMachine$7: StateMachine[scala.concurrent.Promise[Int], scala.concurrent.ExecutionContext] = new stateMachine$7();
283+
scala.concurrent.Future.apply(stateMachine$7.apply())(scala.concurrent.ExecutionContext.Implicits.global);
284+
stateMachine$7.result$async.future
285+
}
286+
287+
## References
288+
289+
1. [The Scala Futures API][1]
290+
2. [The Play! Framework][2]
291+
3. [Scala Async on Github][3]
292+
293+
[1]: http://docs.scala-lang.org/overviews/core/futures.html "ScalaFutures"
294+
[2]: http://www.playframework.com/ "Play"
295+
[3]: https://github.com/scala/async "ScalaAsync"
296+
297+
298+

0 commit comments

Comments
 (0)
0