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