Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add section on using scala3 union types #695

Merged
merged 4 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -138,19 +138,19 @@ class InteractionPatterns3Spec extends ScalaTestWithActorTestKit with AnyWordSpe
sealed trait Command
final case class Translate(site: URI, replyTo: ActorRef[URI]) extends Command

private type CommandAndResponse = Command | Backend.Response
private type CommandAndResponse = Command | Backend.Response // (1)

def apply(backend: ActorRef[Backend.Request]): Behavior[Command] =
def apply(backend: ActorRef[Backend.Request]): Behavior[Command] = // (2)
Behaviors.setup[CommandAndResponse] { context =>

def active(inProgress: Map[Int, ActorRef[URI]], count: Int): Behavior[CommandAndResponse] = {
Behaviors.receiveMessage[CommandAndResponse] {
case Translate(site, replyTo) =>
val taskId = count + 1
backend ! Backend.StartTranslationJob(taskId, site, context.self)
backend ! Backend.StartTranslationJob(taskId, site, context.self) // (3)
active(inProgress.updated(taskId, replyTo), taskId)

case Backend.JobStarted(taskId) =>
case Backend.JobStarted(taskId) => // (4)
context.log.info("Started {}", taskId)
Behaviors.same
case Backend.JobProgress(taskId, progress) =>
Expand All @@ -164,7 +164,7 @@ class InteractionPatterns3Spec extends ScalaTestWithActorTestKit with AnyWordSpe
}

active(inProgress = Map.empty, count = 0)
}.narrow
}.narrow // (5)
}
// #adapted-response

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Handling responses in Scala 3

Handling responses from other actors in Scala 3 is straightforward and in contrast with
Scala 2, it doesn't require the utilisation of message adapters and response wrappers.

A distinction exists between an actor's public protocol (`Command `) and its internal
protocol (`CommandAndResponse`). The latter is the union of the public protocol and all
the responses the actor should understand. This is union is implemented with Scala 3's
Union types.

**Example:**

![adapted-response.png](./images/adapted-response-scala-3.png)

Scala
: @@snip [InteractionPatternsSpec.scala](/actor-typed-tests/src/test/scala-3/docs/org/apache/pekko/typed/InteractionPatterns3Spec.scala) { #adapted-response }

Let's have a look at the key changes with respect to the Pekko typed implementation in
Scala 2 (see the corresponding numbering in the example code).

* The type `CommandAndResponse` is the union of `Command` and `Backend.Response` (1)
* In the factory method (2) for the `Behavior` of the frontend actor, a
`Behavior[CommandAndResponse]` is narrowed (5) to a `Behavior[Command]`. This works as
the former is able to handle a superset of the messages that can be handled by the latter.
* The sending actor just sends its `self` @apidoc[actor.typed.ActorRef] in the `replyTo`
field of the message (3)
* Responses are handled in a straightforward manner (4)

A more in-depth explanation of the concepts used in applying Scala 3's Union types can
be found in the following blog posts:

* [Using Dotty Union types with Akka Typed](https://blog.lunatech.com/posts/2020-02-12-using-dotty-union-types-with-akka-typed)
* [Using Dotty Union types with Akka Typed - Part II](https://blog.lunatech.com/posts/2020-02-19-using-dotty-union-types-with-akka-typed-part-II)

**Useful when:**

* Subscribing to an actor that will send [many] response messages back

**Problems:**

* It is hard to detect that a message request was not delivered or processed
* Unless the protocol already includes a way to provide context, for example a request id
that is also sent in the response, it is not possible to tie an interaction to some
specific context without introducing a new, separate, actor
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions docs/src/main/paradox/typed/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ project.description: Using Apache Pekko to build reliable multi-core application
* [actors](actors.md)
* [actor-lifecycle](actor-lifecycle.md)
* [interaction patterns](interaction-patterns.md)
* [handling responses with Scala 3](handling-actor-responses-with-scala3.md)
* [fault-tolerance](fault-tolerance.md)
* [actor-discovery](actor-discovery.md)
* [routers](routers.md)
Expand Down
11 changes: 9 additions & 2 deletions docs/src/main/paradox/typed/interaction-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ Java

**Problems:**

* Actors seldom have a response message from another actor as a part of their protocol (see @ref:[adapted response](#adapted-response))
* Actors seldom have a response message from another actor as a part of their protocol as it can be considered
as polluting that protocol with a message from another actor's message (see @ref:[adapted response](#adapted-response))
* It is hard to detect that a message request was not delivered or processed (see @ref:[ask](#request-response-with-ask-between-two-actors))
* Unless the protocol already includes a way to provide context, for example a request id that is also sent in the
response, it is not possible to tie an interaction to some specific context without introducing a new,
Expand All @@ -112,7 +113,13 @@ Java

## Adapted Response

Most often the sending actor does not, and should not, support receiving the response messages of another actor. In such cases we need to provide an @apidoc[actor.typed.ActorRef] of the right type and adapt the response message to a type that the sending actor can handle.
Most often the sending actor does not, and should not, support receiving the response messages of another actor.

In such cases we need to provide an @apidoc[actor.typed.ActorRef] of the right type and adapt the response message
to a type that the sending actor can handle. In the case of Scala, we need to make a distinction between Scala 2
and Scala 3. In the latter case, we can actually get rid of the need to adapt the response message by leveraging
Scala 3's Union types, which vastly simplifies the handling of responses. The details can be found in the
section @ref:[Handling actor responses in Scala 3](handling-actor-responses-with-scala3.md).

**Example:**

Expand Down