completed once all futures in the original list are completed val futures: List[Future[Int]] = for (i <- List.range(1, 10)) yield Future { i * 101 } val wanted: Future[List[Int]] = ???
completed once all futures in the original list are completed val futures: List[Future[Int]] = for (i <- List.range(1, 10)) yield Future { i * 101 } val wanted: Future[List[Int]] = Future { futures.map { fut => // await completion of `fut` } } fut.onComplete(...) fut.map(...) fut.onComplete(...) fut.map(...)
completed once all futures in the original list are completed val futures: List[Future[Int]] = for (i <- List.range(1, 10)) yield Future { i * 101 } val wanted: Future[List[Int]] = Future { futures.map { fut => // await completion of `fut` } } Await.result(fut, 100.millis) Blocks the calling thread! Best case: Underlying ExecutionContext supports managed blocking → size of thread pool increases dynamically → performance degradation!
completed once all futures in the original list are completed val futures: List[Future[Int]] = for (i <- List.range(1, 10)) yield Future { i * 101 } val wanted: Future[List[Int]] = Future { futures.map { fut => // await completion of `fut` } } Await.result(fut, 100.millis) Blocks the calling thread! Worst case: Underlying ExecutionContext does not support managed blocking → fewer threads available in thread pool → possible deadlock!
• await has the type we need: def await[T](future: Future[T]): T val futures: List[Future[Int]] = for (i <- List.range(1, 10)) yield Future { i * 101 } val wanted: Future[List[Int]] = Future { futures.map { fut => // await completion of `fut` } } await(fut) Invalid! Limitation of scala-async: await cannot be nested under a lambda! (or a local method, object or class) await(fut)
completed once all futures in the original list are completed val futures: List[Future[Int]] = for (i <- List.range(1, 10)) yield Future { i * 101 } val wanted: Future[List[Int]] = Future { futures.map { fut => // await completion of `fut` } } Await.result(fut, 100.millis) Instead of calling List.map …
completed once all futures in the original list are completed val futures: List[Future[Int]] = for (i <- List.range(1, 10)) yield Future { i * 101 } val wanted: Future[List[Int]] = Future.sequence(futures) We should be calling Future.sequence! From the ScalaDoc: “Simple version of Future.traverse. Asynchronously and non-blockingly transforms, in essence, a IterableOnce[Future[A]] into a Future[IterableOnce[A]]. Useful for reducing many Futures into a single Future.” Essentially, a specialized method to deal with collections of futures.
should only contain numbers less than 500? val futures: List[Future[Int]] = for (i <- List.range(1, 10)) yield Future { i * 101 } val temp: Future[List[List[Int]]] = Future.sequence(futures.map { fut => fut.map(v => if v < 500 then List(v) else List()) }) val wanted: Future[List[Int]] = temp.map(nested => nested.flatten) Is there a simpler way?? The code uses the methods sequence, map for futures, list methods cannot be reused!
• Calling Await.result can lead to performance degradation or deadlock • scala-async does not permit calling await nested under a lambda • Result: Instead of using familiar methods on collections, future-specific methods had to be used • Not being able to use collection methods with concurrent code is a problem • Collections provide hundreds of methods… • … duplicate all those methods for concurrent code?!
List.range(1, 10)) yield Future { i * 101 } val wanted: Future[List[Int]] = Future { futures.flatMap { fut => val v = fut.await if v < 500 then List(v) else List() } } Suspend enclosing future without blocking any underlying OS thread!
10)) yield Future { i * 101 } val temp: Future[List[List[Int]]] = Future.sequence(futures.map { fut => fut.map(v => if v < 500 then List(v) else List()) }) val wanted: Future[List[Int]] = temp.map(nested => nested.flatten)
futures: List[Future[Int]] = for (i <- List.range(1, 10)) yield Future { i * 101 } val wanted: Future[List[Int]] = Future: futures.flatMap: fut => val v = fut.await if v < 500 then List(v) else List()
yield Future { i * 101 } val wanted: Future[List[Int]] = Future: futures.flatMap: fut => val v = fut.await if v < 500 then List(v) else List() Benefits: • Can simply use List.flatMap, no need for Future.sequence and Future.map • No need for intermediate nested list • Effect safety using capabilities (experimental)
library for Scala 3 • Key features: • Lightweight concurrency • This means: • Concurrent tasks are cheap (memory and context switching) • Concurrent tasks can always suspend without blocking any underlying OS thread • On the JVM: based on virtual threads in JDK 21 (see JEP 444) • On Scala Native: based on delimited continuations • Scala.js not supported • Effects as capabilities • Effect polymorphism enables reusing higher-order functions (like List.map) • In the future: ensuring effect safety using capture checking (experimental)
a concurrent task/computation is an effect, requiring resources such as a thread pool • In fact, this effect has been specified in Scala 2 using an implicit parameter: (implicit executor: ExecutionContext) • Suspending a computation in order to wait for the result of a future is an effect • Message passing (send/receive) is an effect • Etc.
• “Implicit parameters” in Scala 2 parlance • Later: as true capabilities using capture checking trait Future[+T] extends ..., Cancellable: ... def awaitResult(using ac: Async): Try[T] object Future: ... def apply[T](body: Spawn ?=> T) (using async: Async, spawnable: Spawn & async.type): Future[T] ?=> introduces a context function where Spawn is a context parameter awaitResult can only be called in a context where a given of type Async is in scope
in green: inferred by type checker @main def main() = Async.blocking { // (using Spawn) => val futures: List[Future[Int]] = for (i <- List.range(1, 10)) yield Future { i * 101 } val wanted: Future[List[Int]] = Future { // (using Spawn) => futures.map(fut => fut.await) } println(wanted.await) } Spawn is like Async (permitting suspension) but adds the capability to spawn concurrent computations Executes body on current thread which suspends upon await Remarks: • Async.blocking is the only way to obtain a Spawn capability • Therefore, every Gears app needs at least one call to Async.blocking!
has an effect E • The effect E could be to (potentially) perform I/O or throw an exception • What should be the effect of the following call to map? • The effect of lst.map(fun) is equal to the effect of fun! • This means List.map is polymorphic in the effect of its argument val lst = List.range(1, 10) lst.map(fun) Lukas Rytz, Martin Odersky, Philipp Haller: Lightweight Polymorphic Effects. ECOOP 2012: 258-282 https://doi.org/10.1007/978-3-642-31057-7_13
S represents pure functions which are effect-free (experimental) • The function type T => S represents effectful functions • Higher-order functions are effect-polymorphic by default • Example: the existing type definition of List.map expresses effect polymorphism: • Therefore, it is possible to pass a closure with Async effect to List.map, and the resulting call to map will have Async effect: abstract class List[+A] extends ...: def map[B](f: A => B): List[B] futures.map(fut => fut.await)
feature for checking and ensuring effect safety • In the context of Gears: • Enforce scoping rules for Async/Spawn contexts • Functions taking an Async capability should not capture the capability such that its lifetime exceeds that of the function’s body • Tutorial on capture checking: • See https://docs.scala-lang.org/scala3/reference/experimental/cc.html • Research paper with more details and theoretical foundations: Aleksander Boruch-Gruszecki, Martin Odersky, Edward Lee, Ondrej Lhoták, Jonathan Immanuel Brachthäuser: Capturing Types. ACM Trans. Program. Lang. Syst. 45(4): 21:1-21:52 (2023) https://doi.org/10.1145/3618003
Scala • Supporting different concurrency models (actors, X10 async/finish, …) • Ownership and ownership transfer in Scala • Other topics: • Concurrent programming, e.g., deterministic concurrency • Distributed programming • Provable failure transparency (Portals: https://www.portals-project.org/) Haller and Loiko. LaCasa: Lightweight af fi nity and object capabilities in Scala. OOPSLA 2016 https://doi.org/10.1145/2983990.2984042 Willenbrink. A Type System for Ensuring Safe, Structured Concurrency in Scala. MSc thesis, KTH Royal Institute of Technology, Sweden, 2024. To appear Haller. Enhancing closures in Scala 3 with Spores3. Scala Symposium 2022: 22-27 https://doi.org/10.1145/3550198.3550428
approach to effects and effect polymorphism • Gears is an experimental concurrency library which builds on • new runtime support for lightweight threads (virtual threads on JDK 21) • Scala 3 contextual abstractions (context parameters, context functions) • Capture checking (experimental) holds great promise to make effect-polymorphic lightweight concurrency safer • Ensure scoping rules for capabilities (Async, Spawn, etc.) • Ongoing research explores data-race freedom