Skip to content

Commit c36dd17

Browse files
committed
Refinements to the README
1 parent 91c1d10 commit c36dd17

File tree

1 file changed

+29
-19
lines changed
  • src/main/scala/progscala3/concurrency/direct

1 file changed

+29
-19
lines changed

src/main/scala/progscala3/concurrency/direct/README.md

+29-19
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ This directory was added in June 2023, well after _Programming Scala, Third Edit
66

77
I recommend viewing [this Martin Odersky talk](https://www.youtube.com/watch?v=0Fm0y4K4YO8) about the subject. The following discussion is based on it.
88

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:
9+
Scala uses monads extensively for many operations with non-trivial effects, such as those involving asynchronous computation, including I/O. The idea of direct style is to allow writing code with non-trivial effects more simply without the boilerplate of monads.
10+
11+
## Motivation
12+
13+
Consider the example of `Future`s. You currently use them like this:
1014

1115
```scala
1216
val sum =
@@ -30,20 +34,20 @@ A new _direct style_ implementation of futures would be used liked this:
3034
```
3135

3236
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`.
37+
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, an implementation based on the `boundary` and `break` mechanism discussed below, this exception would be caught by the `Future` library, used to cancel the other running `Future`, if one is still running, and then return a `Failure` future for `sum`.
3438

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.
39+
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_.
3640

3741
## Implementing Direct Style in Scala
3842

39-
In the talk, Martin discusses four aspects of building support for direct style in Scala:
43+
In Martin's talk, he discusses four aspects of building support for direct style in Scala:
4044

4145
1. `boundary` and `break` - now available in Scala 3.3.0.
4246
2. Error handling - enabled, but not yet used in the library, which is still the Scala 2.13 library.
4347
3. Suspensions - work in progress.
44-
3. Concurrent library design built on the above - work in progress.
48+
4. Concurrent library design built on the above - work in progress.
4549

46-
## `boundary` and `break`
50+
### `boundary` and `break`
4751

4852
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.
4953

@@ -66,6 +70,8 @@ def firstIndex[T](xs: List[T], elem: T): Int =
6670

6771
As shown, `break` can optionally return a value.
6872

73+
This is one way to return the index for an element found in a collection or -1 if not found. The `boundary` defines the scope where a non-local return may be invoked. If the desired element `elem` is found, then we call `break` to return the index. This breaks out of the `for` comprehension, too, since there is no point in continuing. If `elem` isn't found, `-1` is returned through the normal return path.
74+
6975
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)):
7076

7177
```scala
@@ -84,37 +90,41 @@ object boundary:
8490
end boundary
8591
```
8692

87-
In the example above, the `boundary:` line is short for `boundary.apply:` with the indented code below it passed as the body.
93+
In the `firstIndex` example above, the `boundary:` line is short for `boundary.apply:` with the indented code below it passed as the body.
8894

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`.
95+
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 inside the implementation (not shown) 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`.
9096

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.
97+
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 the code block you provide. If your code block calls `break`, a given `Label`will be in-scope.
9298

9399
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.
94100

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`.
101+
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.
98102

99-
### Other Benefits
103+
#### Other Benefits
100104

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:
105+
The `boundary` and `break` mechanism 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 mechanism is easier for developers to use and it adds the following additional benefits:
102106

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.
107+
* 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, non-local `break`s are logically implemented as non-fatal exceptions and the implementation is optimized to suppress unnecessary stack trace generation. Stack traces are unnecessary because we are handling these exceptions, not barfing them on the user!
104108
* 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.
105109

106-
## A New Concurrency Library
110+
### Error Handling
111+
112+
This directory includes a second example, `optional.scala` that is discussed in the video as a first step towards new ways of handling errors, built on `boundary` and `break` and partly inspired by the way Rust handles errors. This simpler example tries a computation that will hopefully return a result wrapped in a `Some`, but if it can't succeed, then return `None`. Hence, this handles some "optional" behavior. What's lost is all information about _why_ it failed. An error-handling extension would add this information.
113+
114+
See the comments in that file for details and an example of usage in `BoundaryExamples.scala`.
115+
116+
### Suspensions and a New Concurrency Library
107117

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,
118+
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, as well `continuations`, which Martin is calling `suspensions`. I won't all discuss the details he covered here, but I will mention a few things. The implementation is not trivial for Scala, because:
109119

110120
1. Scala now runs on three platforms: JVM, JavaScript, and native.
111121
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).
112122

113-
Possible implementation approaches include using source or bytecode rewriting.
123+
Besides writing custom implementations for each of these scenarios, other possible implementations might use source or bytecode rewriting.
114124

115125
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.
116126

117-
### A Comparison with Ray
127+
#### A Comparison with Ray
118128

119129
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++.
120130

0 commit comments

Comments
 (0)