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

Scala
source
object Backend { sealed trait Request final case class StartTranslationJob(taskId: Int, site: URI, replyTo: ActorRef[Response]) extends Request sealed trait Response final case class JobStarted(taskId: Int) extends Response final case class JobProgress(taskId: Int, progress: Double) extends Response final case class JobCompleted(taskId: Int, result: URI) extends Response } object Frontend { sealed trait Command final case class Translate(site: URI, replyTo: ActorRef[URI]) extends Command private type CommandAndResponse = Command | Backend.Response // (1) 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) // (3) active(inProgress.updated(taskId, replyTo), taskId) case Backend.JobStarted(taskId) => // (4) context.log.info("Started {}", taskId) Behaviors.same case Backend.JobProgress(taskId, progress) => context.log.info2("Progress {}: {}", taskId, progress) Behaviors.same case Backend.JobCompleted(taskId, result) => context.log.info2("Completed {}: {}", taskId, result) inProgress(taskId) ! result active(inProgress - taskId, count) } } active(inProgress = Map.empty, count = 0) }.narrow // (5) }

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 ActorRefActorRef 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:

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