|
| 1 | +# README for `direct` |
| 2 | + |
| 3 | +> **Note:** A more complete version of this text can be found at [Dean's Scala 3 blog](https://medium.com/scala-3). |
| 4 | +
|
| 5 | +This directory was added in June 2023, well after _Programming Scala, Third Edition_ was published. The code in this section illustrates the new _direct style_ that was added to Scala starting in version 3.3.0. |
| 6 | + |
| 7 | +I recommend viewing [this Martin Odersky talk](https://www.youtube.com/watch?v=0Fm0y4K4YO8) about the subject. The following discussion is based on it. |
| 8 | + |
| 9 | +The idea of _direct style_ is to explore how we can create and use primitives, such as concurrency abstractions, that don't require the boilerplate of monads. For motivation, consider the example of `Future`s. You currently use them like this: |
| 10 | + |
| 11 | +```scala |
| 12 | +val sum = |
| 13 | + val f1 = Future(c1.read) |
| 14 | + val f2 = Future(c2.read) |
| 15 | + for |
| 16 | + x <- f1 |
| 17 | + y <- f2 |
| 18 | + yield x + y |
| 19 | +``` |
| 20 | + |
| 21 | +The `for` comprehension invokes `map` and `flatMap` to wait on the futures and extract the values. |
| 22 | + |
| 23 | +A new _direct style_ implementation of futures would be used liked this: |
| 24 | + |
| 25 | +```scala |
| 26 | + val sum = Future: |
| 27 | + val f1 = Future(c1.read) |
| 28 | + val f2 = Future(c2.read) |
| 29 | + f1.value + f2.value |
| 30 | +``` |
| 31 | + |
| 32 | +In both cases, the two futures are executing in parallel, which might take a while. The |
| 33 | +sum of the returned values is returned in a new future. The `value` method returns the result of a future once it is available or it throws an exception if the future returns a `Failure`. However, using the `boundary` and `break` mechanism discussed below, this exception would be caught by the `Future` library and used to cancel the other running `Future`, if one is still running, and then return a "failed" future for `sum`. |
| 34 | + |
| 35 | +So, direct style simplifies code, making it is easier to write and understand, plus it enables cleaner separation of concerns, such as handling timeouts and failures in futures, and it cleanly supports composability, which monads don't provide unless you use cumbersome machinary such as monad transformers. |
| 36 | + |
| 37 | +## Implementing Direct Style in Scala |
| 38 | + |
| 39 | +In the talk, Martin discusses four aspects of building support for direct style in Scala: |
| 40 | + |
| 41 | +1. `boundary` and `break` - now available in Scala 3.3.0. |
| 42 | +2. Error handling - enabled, but not yet used in the library, which is still the Scala 2.13 library. |
| 43 | +3. Suspensions - work in progress. |
| 44 | +3. Concurrent library design built on the above - work in progress. |
| 45 | + |
| 46 | +## `boundary` and `break` |
| 47 | + |
| 48 | +This mechanism is defined with a new addition to the API, [`scala.util.boundary$`](https://www.scala-lang.org/api/3.3.0/scala/util/boundary$.html). It provides a cleaner alternative to non-local returns. |
| 49 | + |
| 50 | +* `boundary` defines a context for a computation. |
| 51 | +* `break` returns a value from within the enclosing boundary. |
| 52 | + |
| 53 | +Here is an example from Martin's talk, which is also discussed in the [API](https://www.scala-lang.org/api/3.3.0/scala/util/boundary$.html): |
| 54 | + |
| 55 | +```scala |
| 56 | +import scala.util.boundary, boundary.break |
| 57 | + |
| 58 | +def firstIndex[T](xs: List[T], elem: T): Int = |
| 59 | + boundary: |
| 60 | + for (x, i) <- xs.zipWithIndex do |
| 61 | + if x == elem then break(i) |
| 62 | + -1 |
| 63 | +``` |
| 64 | + |
| 65 | +`BoundaryExamples.scala` in this directory also implements this method. (There is a test for it in `.../test/scala/concurrency/direct/BoundaryExamplesSuite.scala`.) |
| 66 | + |
| 67 | +As shown, `break` can optionally return a value. |
| 68 | + |
| 69 | +Here is a slightly simplified implementation. (The full 3.3.0 source code is [here](https://github.com/lampepfl/dotty/blob/3.3.0/library/src/scala/util/boundary.scala)): |
| 70 | + |
| 71 | +```scala |
| 72 | +package scala.util |
| 73 | + |
| 74 | +object boundary: |
| 75 | + final class Label[-T] |
| 76 | + |
| 77 | + inline def apply[T](inline body: Label[T] ?=> T): T = ... |
| 78 | + |
| 79 | + def break[T](value: T)(using label: Label[T]): Nothing = |
| 80 | + throw Break(label, value) |
| 81 | + |
| 82 | + final class Break[T] (val label: Label[T], val value: T) extends RuntimeException(...) |
| 83 | + |
| 84 | +end boundary |
| 85 | +``` |
| 86 | + |
| 87 | +In the example above, the `boundary:` line is short for `boundary.apply:` with the indented code below it passed as the body. |
| 88 | + |
| 89 | +Well actually, the `block` passed to `apply` is a _context function_ that is called within `apply` to return the block of code shown in the example. Note the `final class Label[T]` declaration in `boundary`. Users don't define `Label` instances themselves. Instead, this is done by the implementation of `boundary.apply` to provide the _capability_ of doing a non-local return. Using a `Label` in this way prevents the user from trying to call `break` without an enclosing `boundary`. |
| 90 | + |
| 91 | +Rephrasing all that, we don't want users to call `break` without an enclosing `boundary`. That's why `break` requires an in-scope given instance of `Label`, which the implementation of `boundary.apply` creates before it calls `body` (not shown). When your code block is executed, if it calls `break`, a given `Label` is in-scope. |
| 92 | + |
| 93 | +You don't have to do anything to create the context function passed to `boundary.apply`. It is synthesized from your block of code automatically when `boundary.apply` is called. |
| 94 | + |
| 95 | +Look at `firstIndex()` again. If we do find an element that is equal to `elem`, then we call break to return the index `i` from the `boundary`. If we don't find the element, then a "normal" return is used to return `-1`. We never reach the `-1` expression if `break` is called. |
| 96 | + |
| 97 | +This directory includes a second example, `optional.scala` that is discussed in the video. See the comments in that file for details and an example of usage in `BoundaryExamples.scala`. |
| 98 | + |
| 99 | +### Other Benefits |
| 100 | + |
| 101 | +This implementation is a better alternative to `scala.util.control.NonLocalReturns` and `scala.util.control.Breaks` which are deprecated as of Scala 3.3.0. The new feature is easier for developers to use and it adds the following additional benefits: |
| 102 | + |
| 103 | +* The implementation uses a new [`scala.util.boundary$.Break`](https://www.scala-lang.org/api/3.3.0/scala/util/boundary$$Break.html) class that derives from `RuntimeException`. Therefore, `break`s are logically non-fatal exceptions and the implementation is optimized to suppress unnecessary stack trace generation. |
| 104 | +* Better performance is provided when a `break` occurs to the enclosing scope inside the same method (i.e., the same stack frame), where it can be rewritten to a jump call. |
| 105 | + |
| 106 | +## A New Concurrency Library |
| 107 | + |
| 108 | +Back to the futures :) , `boundary` and `break` can be used for adding new concurrency abstractions to Scala following a direct style, like the `Futures` example above. The implementation is not trivial for Scala, because, |
| 109 | + |
| 110 | +1. Scala now runs on three platforms: JVM, JavaScript, and native. |
| 111 | +2. Even on the JVM, using the new lightweight fibers coming in [Project Loom](https://wiki.openjdk.org/display/loom/Main) would only be available to users on the most recent JVMs (19 and later). |
| 112 | + |
| 113 | +Possible implementation approaches include using source or bytecode rewriting. |
| 114 | + |
| 115 | +So, the implementation will be non-trivial, but work has started in the [`lampepfl/async`](https://github.com/lampepfl/async) repo, a "strawman" for ideas, both for conceptual abstractions for concurrency (like a new `Future` type), as well as implementations. |
| 116 | + |
| 117 | +### A Comparison with Ray |
| 118 | + |
| 119 | +The direct style for `Futures` above looks a lot like working with tasks and actors in [Ray](https://ray.io), the Python-centric concurrency library that is becoming popular for computationally-heavy projects, like ML/AI. [I really like that API](https://medium.com/distributed-computing-with-ray/ray-for-the-curious-fa0e019e17d3) for its simplicity and concision for users. The Ray abstractions heavily rely on the metaprogramming flexibility in a dynamically-typed language like Python, while the highly-scalable, backend services for distributed computation are written in C++. |
| 120 | + |
| 121 | +In contrast, the Scala abstractions are based on more principled and flexible foundations that are part of Scala 3 itself (discussed below), which keep the implementations very concise. However, the implementations target different users. Ray is designed for large-scale cluster computation, as well as local multi-threading. The equivalent in Scala would require more extensive backend libraries, such as an optional Akka implementation. |
0 commit comments