Skip to content

Commit

Permalink
docs: final refinements
Browse files Browse the repository at this point in the history
  • Loading branch information
tassiluca committed Mar 4, 2024
1 parent 94d38aa commit 7a77702
Show file tree
Hide file tree
Showing 6 changed files with 60 additions and 31 deletions.
2 changes: 0 additions & 2 deletions docs/content/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,3 @@ Here's the outline of the conducted analysis:
5. [Conclusions](./docs/05-going-further)

Code has been organized in Gradle submodules, one for each version of the examples (current monadic futures, Scala Gears, Kotlin Coroutines):


33 changes: 22 additions & 11 deletions docs/content/docs/01-boundaries.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@ bookToc: false

# `boundary` & `break`

[[Source code](https://github.com/lampepfl/dotty/blob/3.3.0-RC4/library/src/scala/util/boundary.scala)]
- [`boundary` \& `break`](#boundary--break)
- [Modeling error handling data types with non-local breaks](#modeling-error-handling-data-types-with-non-local-breaks)
- [`Optional`](#optional)
- [`Either` + `?`](#either--)

`boundary` & `break` mechanism provides a cleaner alternative to non-local returns:

- `boundary:` is short for `boundary.apply:`
- the indented code below it is passed as `body` is a context function that is called within `boundary.apply`
- to `break` an in-scope `given` instance of `Label` is required (i.e. is impossible `break` without an enclosing `boundary`)
- Users don't define `Label` instances themselves. Instead, this is done inside the implementation of `boundary.apply` to provide the capability of doing a non-local return
- the indented code below, passed as `body`, is a context function that is called within `boundary.apply`
- to `break`, an in-scope `given` instance of `Label` is required (i.e. is impossible to `break` without an enclosing `boundary`)
- Users don't define `Label` instances themselves. Instead, this is done inside the implementation of `boundary.apply` to provide the capability of doing a non-local return [[Ref](https://github.com/lampepfl/dotty/blob/3.3.0-RC4/library/src/scala/util/boundary.scala)]:
```scala
// From the Scala 3 standard library
/** Run `body` with freshly generated label as implicit argument.
* Catch any breaks associated with that label and return their
* results instead of `body`'s result.
Expand All @@ -34,9 +38,11 @@ But, most importantly, they **lay the foundations** (along with a **`resume` mec

{{< /hint >}}


## Modeling error handling data types with non-local breaks

In the following section are presented two data types that can be used to handle errors, both leveraging the `boundary` and `break` mechanism.
The first (`optional`) has been presented in the [Scalar conference by M. Odersky](https://www.google.com/search?client=safari&rls=en&q=direct+style+odersky&ie=UTF-8&oe=UTF-8), while the second has been implemented to apply the same style also to `Either` data type.

### `Optional`

```scala
Expand All @@ -59,7 +65,7 @@ object optional:
### `Either` + `?`

```scala
/** Represents a computation that will hopefully return a [[Right]] value, but might fail with a [[Left]] one. */
/** Represents a computation that will hopefully return a [[Right]] value but might fail with a [[Left]] one.*/
object either:

/** Defines the boundary for the [[Either]] returning computation, whose [[body]] is given in input. */
Expand All @@ -76,12 +82,13 @@ object either:
case Left(value) => break(Left(value))

extension [R](t: Try[R])
/** @return this [[Success]] value or break to the enclosing boundary with a [[Left]] containing the converted
* `Throwable` exception performed by the implicit [[converter]].
/** @return this [[Success]] value or break to the enclosing boundary with a [[Left]] containing
* the converted `Throwable` exception performed by the implicit [[converter]].
*/
inline def ?[L](using Label[Left[L, Nothing]])(using converter: Conversion[Throwable, L]): R = t match
case Success(value) => value
case Failure(exception) => break(Left(converter(exception)))
inline def ?[L](using Label[Left[L, Nothing]])(using converter: Conversion[Throwable, L]): R =
t match
case Success(value) => value
case Failure(exception) => break(Left(converter(exception)))

/** An object encapsulating a collection of `Throwable` given converters. */
object EitherConversions:
Expand All @@ -91,3 +98,7 @@ object EitherConversions:
```

This kind of data type will be particularly useful in the next examples to quickly break in case of failures, returning the caller a meaningful error message, and simplifying the error-handling code.

{{< button href="https://tassiluca.github.io/PPS-22-direct-style-experiments/PPS-22-direct-style-experiments/" >}} **Home** {{< /button >}}

{{< button href="https://tassiluca.github.io/PPS-22-direct-style-experiments/PPS-22-direct-style-experiments/docs/02-basics/" >}} **Next**: Basic asynchronous constructs {{< /button >}}
32 changes: 20 additions & 12 deletions docs/content/docs/02-basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,8 @@ The other important key feature of the library is the support for **structured c
// this can be interrupted
```

- `Future`s are nestable; **the lifetime of nested computations is contained within the lifetime of enclosing ones**. This is achieved using `CompletionGroup`s, which are cancellable objects themselves and serve as containers for other cancellable objects, that once canceled, all of its members are canceled as well.
- `Future`s are nestable; **the lifetime of nested computations is contained within the lifetime of enclosing ones**. This is achieved using **`CompletionGroup`s**, which are cancellable objects themselves and serve as **containers for other cancellable objects**; **once they are canceled, all of their members are canceled as well**.
- The group is accessible through `Async.current.group`;
- A cancellable object can be included inside the cancellation group of the async context using the `link` method; this is what the [implementation of the `Future` does, under the hood](https://github.com/lampepfl/gears/blob/07989ffdae153b2fe11ac1ece53ce9dd1dbd18ef/shared/src/main/scala/async/futures.scala#L140).

The implementation of the `create` function with direct style in gears looks like this:
Expand Down Expand Up @@ -401,9 +402,10 @@ Some remarks:
val (post, author) = contentVerifier.zip(authorizer).awaitResult.?
```

👉🏻 To showcase the structured concurrency and cancellation mechanisms of Scala Gears tests have been prepared:
- [`StructuredConcurrencyTest`](https://github.com/tassiLuca/PPS-22-direct-style-experiments/blob/master/commons/src/test/scala/io/github/tassiLuca/StructuredConcurrencyTest.scala)
- [`CancellationTest`](https://github.com/tassiLuca/PPS-22-direct-style-experiments/blob/master/commons/src/test/scala/io/github/tassiLuca/CancellationTest.scala)
👉🏻 To showcase the structured concurrency and cancellation mechanisms of Scala Gears tests have been prepared:

- [`StructuredConcurrencyTest`](https://github.com/tassiLuca/PPS-22-direct-style-experiments/blob/master/commons/src/test/scala/io/github/tassiLuca/dse/StructuredConcurrencyTest.scala)
- [`CancellationTest`](https://github.com/tassiLuca/PPS-22-direct-style-experiments/blob/master/commons/src/test/scala/io/github/tassiLuca/dse/CancellationTest.scala)

Other combinator methods, available on `Future`s instance:

Expand Down Expand Up @@ -435,17 +437,17 @@ Other combinator methods, available on `Future`s instance:
}
```

Every coroutine must be executed in a coroutine context which is a collection of key-value pairs that provide contextual information for the coroutine, including a dispatcher, that determines what thread or threads the coroutine uses for its execution, and the `Job` of the coroutine, which represents a cancellable background piece of work with a life cycle that culminates in its completion.
Every coroutine must be executed in a coroutine context, which is a collection of key-value pairs that provide contextual information for the coroutine, including a dispatcher, that determines what thread or threads the coroutine uses for its execution, and the `Job` of the coroutine, which represents a cancellable background piece of work with a life cycle that culminates in its completion.

- Different ways to create a scope:
- `GlobalScope.launch` launching a new coroutine in the global scope -- *discouraged because it can lead to memory leaks*
- `CoroutineScope(Dispatchers.Default)`, using the constructor with a dispatcher
- `runBlocking` - equivalent to the Gears `Async.blocking` - provides a way to run a coroutine in the `MainScope`, i.e. on the main/UI thread
- `GlobalScope.launch` launching a new coroutine in the global scope -- *discouraged because it can lead to memory leaks*;
- `CoroutineScope(Dispatchers.Default)`, using the constructor with a dispatcher;
- `runBlocking` - equivalent to the Gears `Async.blocking` - provides a way to run a coroutine in the `MainScope`, i.e. on the main/UI thread.

- Useful dispatchers:
- `Default dispatcher`: to run CPU-intensive functions. If we forget to choose our dispatcher, this dispatcher will be selected by default.
- `IO dispatcher`: to run I-O bound computation, where we block waiting for input-output operations to complete, like network-related operations, file operations, etc.
- `Unconfined dispatcher`: it isn't restricted to a particular thread, i.e. doesn't change the thread of the coroutine, it operates on the same thread where it was initiated.
- `Default dispatcher`: to run CPU-intensive functions. If we forget to choose our dispatcher, this dispatcher will be selected by default;
- `IO dispatcher`: to run I-O bound computation, where we block waiting for input-output operations to complete, like network-related operations, file operations, etc.;
- `Unconfined dispatcher`: it isn't restricted to a particular thread, i.e. doesn't change the thread of the coroutine, it operates on the same thread where it was initiated;
- `Main dispatcher`: used when we want to interact with the UI. It is restricted to the main thread.

- Several coroutine builders exist, like `launch`, `async`, `withContext` which accept an optional `CoroutineContext` parameter that can be used to specify the dispatcher and other context elements.
Expand Down Expand Up @@ -508,11 +510,17 @@ private suspend fun verifyContent(title: String, body: String): PostContent { ..

- a `coroutineScope` is a suspending function used to create a new coroutine scope: it suspends the execution of the current coroutine, releasing the underlying thread for other usages;
- As we said previously, the failure of a child with an exception immediately cancels its parent and, consequently, all its other children: this means that, for handling the cancellation of nested coroutines, we don't need to do anything special, it is already automatically handled by the library.
- [No matter the order in which coroutines are awaited, if one of them fails all the others get cancelled](https://github.com/tassiLuca/PPS-22-direct-style-experiments/blob/master/commons/src/test/kotlin/io/github/tassiLuca/dse/CancellationTest.kt)
- with `coroutineScope` no matter the order in which coroutines are awaited, if one of them fails with an exception it is propagated upwards, cancelling all other ones
- this is not the case for `supervisorScope`, a coroutine builder ensuring that child coroutines can fail independently without affecting the parent coroutine.
- have a look to [this test](https://github.com/tassiLuca/PPS-22-direct-style-experiments/blob/master/blog-ws-direct-kt/src/test/kotlin/io/github/tassiLuca/dse/CoroutinesCancellationTests.kt)
- This is an advantage over the Scala Gears, where operators like `zip` and `altWithCancel` are necessary!

## Takeaways

> - Scala Gears offers, despite the syntactical differences, very similar concepts to Kotlin Coroutines, with structured concurrency and cancellation mechanisms;
> - Kotlin Coroutines handles the cancellation of nested coroutines more easily than Scala Gears, where special attention is required;
> - As [stated by M. Odersky](https://github.com/lampepfl/gears/issues/19#issuecomment-1732586362) the `Async` capability is better than `suspend` in Kotlin because let defines functions that work for synchronous as well as asynchronous function arguments.
{{< button href="https://tassiluca.github.io/PPS-22-direct-style-experiments/PPS-22-direct-style-experiments/docs/01-boundaries" >}} **Previous**: boundary & break{{< /button >}}

{{< button href="https://tassiluca.github.io/PPS-22-direct-style-experiments/PPS-22-direct-style-experiments/docs/03-channels/" >}} **Next**: Channels as a communication primitive {{< /button >}}
10 changes: 8 additions & 2 deletions docs/content/docs/03-channels.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ As usual, it has been implemented using monadic `Future`s, as well as using Scal

### Future monadic version

[[The sources are available inside the `analyzer-monadic` submodule](https://github.com/tassiLuca/PPS-22-direct-style-experiments/tree/master/blog-ws-monadic/src/main/scala/io/github/tassiLuca/dse/blogv).]
[[The sources are available inside the `analyzer-monadic` submodule](https://github.com/tassiLuca/PPS-22-direct-style-experiments/tree/master/blog-ws-monadic/src/main/scala/io/github/tassiLuca/dse/blog).]

The entry point of the library is the `Analyzer` interface which takes in input the organization name and a function through which is possible to react to results while they are computed.

Expand Down Expand Up @@ -464,6 +464,8 @@ or having set an environment variable named `GH_TOKEN`.

### Kotlin Coroutines version

[[The sources are available inside the `analyzer-direct-kt` submodule](https://github.com/tassiLuca/PPS-22-direct-style-experiments/tree/master/analyzer-direct-kt/src/main/kotlin/io/github/tassiLuca/analyzer).]

The analyzer interface reflects the Scala Gears one: a `Result` is used in place of `Either`, and the suspendable function `udateResults` is marked with the `suspend` keyword in place of the `using Async` context.

```kotlin
Expand Down Expand Up @@ -802,11 +804,15 @@ Success(The Tell-Tale Heart)

{{< /columns >}}

[More tests on `Flows` can be found in `commons`, `pimping` pakcage](https://github.com/tassiLuca/PPS-22-direct-style-experiments/blob/master/commons/src/test/scala/io/github/tassiLuca/pimping/FlowTest.scala).
👉🏻 [More tests on `Flows` can be found in `commons`, `pimping` pakcage](https://github.com/tassiLuca/PPS-22-direct-style-experiments/blob/master/commons/src/test/scala/io/github/tassiLuca/pimping/FlowTest.scala).

## Takeaways

> - `Channel`s are the basic communication and synchronization primitive for exchanging data between `Future`s/`Coroutine`s.
> - Scala Gears support for `Terminable` channels or a review of the closing mechanism should be considered.
> - The `Flow` abstraction in Kotlin Coroutines is a powerful tool for handling cold streams of data, and it is a perfect fit for functions that need to return a stream of asynchronously computed values **by request**.
> - A similar abstraction can be implemented in Scala Gears leveraging `Task`s and `TerminableChannel`s, enabling improved support for an asynchronous flow of data also in Gears, which is currently lacking.
{{< button href="https://tassiluca.github.io/PPS-22-direct-style-experiments/PPS-22-direct-style-experiments/docs/02-basics" >}} **Previous**: Basic asynchronous constructs{{< /button >}}

{{< button href="https://tassiluca.github.io/PPS-22-direct-style-experiments/PPS-22-direct-style-experiments/docs/04-rears" >}} **Next**: Reactivity in direct style{{< /button >}}
9 changes: 8 additions & 1 deletion docs/content/docs/04-rears.md
Original file line number Diff line number Diff line change
Expand Up @@ -548,4 +548,11 @@ suspend fun run(sensorSource: Flow<TemperatureEntry>) {
}
```

## Conclusions
## Takeaways

- Channels in Scala Gears are fine to model flow of data **that exist without application's request from them**: incoming network connections, event streams, etc...
-

{{< button href="https://tassiluca.github.io/PPS-22-direct-style-experiments/PPS-22-direct-style-experiments/docs/03-channels" >}} **Previous**: Channels as a communication primitive{{< /button >}}

{{< button href="https://tassiluca.github.io/PPS-22-direct-style-experiments/PPS-22-direct-style-experiments/docs/05-conclusions" >}} **Next**: Conclusions{{< /button >}}
5 changes: 2 additions & 3 deletions docs/content/docs/05-conclusions.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@ bookToc: false

## Recap

- `Channel`s are the basic communication primitive for exchanging data between `Future`s/`Coroutines` and they are primarily used to model data sources that are *intrinsically hot*, i.e. **that exist without application's request from them**: incoming network connections, event streams, etc...
- `Channel`s are the basic communication primitive for exchanging data between `Future`s/`Coroutines` and they are primarily used to model data sources that are *intrinsically hot*, i.e.
- `Flow`s are control structures, containing executable code. When we call the `collect` method we invoke the code inside the flow, like executing function's code by calling it.



## Conclusions and Final Considerations

To conclude, in this project, the main direct-style asynchronous programming abstractions offered by the Kotlin Coroutines and Scala's new, still entirely experimental library proposal, Gears, were analyzed.
During the analysis, input was also provided for its possible extension where it was deemed lacking compared to the Kotlin framework.

{{< button href="https://tassiluca.github.io/PPS-22-direct-style-experiments/PPS-22-direct-style-experiments/" >}} **Home** {{< /button >}}

0 comments on commit 7a77702

Please sign in to comment.