diff --git a/actor-typed-tests/src/test/scala-3/docs/org/apache/pekko/typed/InteractionPatterns3Spec.scala b/actor-typed-tests/src/test/scala-3/docs/org/apache/pekko/typed/InteractionPatterns3Spec.scala index 26257fe287..404b955208 100644 --- a/actor-typed-tests/src/test/scala-3/docs/org/apache/pekko/typed/InteractionPatterns3Spec.scala +++ b/actor-typed-tests/src/test/scala-3/docs/org/apache/pekko/typed/InteractionPatterns3Spec.scala @@ -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) => @@ -164,7 +164,7 @@ class InteractionPatterns3Spec extends ScalaTestWithActorTestKit with AnyWordSpe } active(inProgress = Map.empty, count = 0) - }.narrow + }.narrow // (5) } // #adapted-response diff --git a/docs/src/main/paradox/typed/handling-actor-responses-with-scala3.md b/docs/src/main/paradox/typed/handling-actor-responses-with-scala3.md new file mode 100644 index 0000000000..5f5cee9092 --- /dev/null +++ b/docs/src/main/paradox/typed/handling-actor-responses-with-scala3.md @@ -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 \ No newline at end of file diff --git a/docs/src/main/paradox/typed/images/adapted-response-scala-3.png b/docs/src/main/paradox/typed/images/adapted-response-scala-3.png new file mode 100644 index 0000000000..1e9ec17684 Binary files /dev/null and b/docs/src/main/paradox/typed/images/adapted-response-scala-3.png differ diff --git a/docs/src/main/paradox/typed/index.md b/docs/src/main/paradox/typed/index.md index e0b096faa8..b162cf62e5 100644 --- a/docs/src/main/paradox/typed/index.md +++ b/docs/src/main/paradox/typed/index.md @@ -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) diff --git a/docs/src/main/paradox/typed/interaction-patterns.md b/docs/src/main/paradox/typed/interaction-patterns.md index 6c2506d120..fac3197166 100644 --- a/docs/src/main/paradox/typed/interaction-patterns.md +++ b/docs/src/main/paradox/typed/interaction-patterns.md @@ -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, @@ -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:**