Using Dotty Union types with Akka Typed
Introduction
I’ve been using Akka Typed for some time, and when I read the blog article titled "Scala 2 Roadmap Update - The Road to Scala 3" on scala-lang.org, it reminded me of an idea I had when I learned about Union types in Dotty. More specifically, in the QA section of the article under the Migrating The Ecosystem section, specific instructions were given about how to use Scala 2.13 binaries with Dotty source code. With that, I had all I needed to start experimenting and to check that my idea was sound. If so, the combination of Akka Typed with Dotty would enable an important simplification of the way responses to messages are handled in Akka Typed with Scala 2.
We’ll start from a simple actor based application written in Scala 2 that uses Akka 2.6.1, and we’ll walk through 3 stages:
-
Initial Scala 2/Akka 2.6.1 application
-
Make the application run as-is on Dotty/Akka 2.6.1
-
Take advantage of Dotty’s Union types to simplify the code
A simple sample Akka 2.6/Scala 2 application
The Scala 2/Akka 2.6.1 application consists of two actors, a Pinger
and a
PingPong
actor. Furthermore, there’s a main program that will drive the
"pinging" process and signal the intent to terminate the program to the Pinger
actor after a preset number of pings. The following figure shows the main
components of the application and the messages flowing between them.
PingPong
applicationThe fact that the application uses Akka Typed implies a number of things:
-
Each actor has a strictly defined protocol, and any code that violates the protocol will not compile.
-
Unlike with Akka Classic, there’s no implicit
sender()
that can be used to respond to the sender of a message. With Akka Typed, we have to explicitly send an appropriately typedActorRef
as part of a message so that the receiver can use it to send a response.
Let’s have a look at the protocol of the actors in our code:
Pinger
and PingPong
actorsAs can be seen, the Pinger
actor "understands" the SendPing
and
StopPingPong
messages while the PingPong
actor understands the Ping
message.
Combining the flow of messages in the application with the protocol definitions,
we see that we have to solve a problem: the Pinger
actor needs to "understand"
the response message from the PingPong
actor. Let’s have a look at the code of
the protocol definition of both actors:
object PingPong {
sealed trait Command
final case class Ping(replyTo: ActorRef[Response]) extends Command
sealed trait Response
case object Pong extends Response
...
}
object Pinger {
sealed trait Command
case object SendPing extends Command
case object StopPingPong extends Command
...
}
Obviously, there’s something missing in the Pinger
protocol definition: it has
to provide a way to understand the response from the PingPong
actor. We
somehow need to extend the Command
ADT to achieve this. In Scala 2 and Akka
2.6 this is achieved by using message adapters.
The following figure shows the updated protocol definition.
Pinger
and PingPong
actorsThe corresponding code looks as follows:
object Pinger {
sealed trait Command
case object SendPing extends Command
case object StopPingPong extends Command
final case class WrappedPongResponse(pong: PingPong.Response)
extends Command (1)
def apply(pingPong: ActorRef[PingPong.Ping]): Behavior[Command] =
Behaviors.setup { context =>
val pongResponseMapper: ActorRef[PingPong.Response] =
context.messageAdapter(response => WrappedPongResponse(response)) (2)
Behaviors.receiveMessage {
case StopPingPong =>
context.log.info(s"End of ping-pong game")
context.system.terminate()
Behaviors.stopped
case SendPing =>
pingPong ! PingPong.Ping(replyTo = pongResponseMapper) (3)
Behaviors.same
case WrappedPongResponse(response) => (4)
context.log.info(s"Hey: I just received a $response !!!")
Behaviors.same
}
}
}
Let’s explain this step-by-step: to start, we extend the Command
ADT by adding
a new message that wraps the response (➊). Next, we use the messageAdaptor
method on the ActorContext
to create a message adapter (➋). The type of the
latter is an ActorRef[PingPong.Response]
. The message adapter is sent along to
the PingPong
actor as a field in the Ping
message (➌). Finally, we receive
the wrapped response (➍).
I think it’s fair to state that this feels a bit boilerplate-ish and it obfuscates the simple operation of receiving a message from another actor. We’ll see how Dotty can simplify this in the last section of this article.
The full code and instructions about how to run this specific step can be found on this repository.
Running the application using Dotty instead of Scala 2
If we want to explore features unique to Dotty, which will become Scala 3 near the end of 2020, with the application described in the previous paragraph, we first need a way to run an application that uses libraries such as the Akka 2.16.1 library that was built with Scala 2.13. The Dotty folks describe how to do exactly this in this paragraph of the main README of the Dotty Example Project.
We just need to make a few changes to our sbt build definition and load the Dotty sbt plugin:
$ cat build.sbt
val dottyVersion = "0.22.0-RC1"
lazy val root = project
.in(file("."))
.settings(
name := "dotty-simple",
version := "0.1.0",
//scalaVersion := dottyLatestNightlyBuild.get,
scalaVersion := dottyVersion,
libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.3",
libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-actor-typed" % "2.6.1",
"com.typesafe.akka" %% "akka-slf4j" % "2.6.1",
"org.scalatest" %% "scalatest" % "3.1.0" % "test",
).map(_.withDottyCompat(scalaVersion.value)) (1)
)
$ cat project/plugins.sbt
addSbtPlugin("ch.epfl.lamp" % "sbt-dotty" % "0.4.0")
So, not much going on here apart from applying
withDottyCompat(scalaVersion.value))
to all cross-built libraries.
We’re now ready for the final step in our experiment!
Using Dotty’s Union types with Akka Typed
Dotty introduces two new types for us to use, Intersection and Union types. In a certain way, Intersection types are the counterpart of Union types. Let’s focus on the latter for this article.
From the Dotty reference documentation, we find the following definition for Union types:
Interesting… and this brings us to my original idea about using Union types
together with Akka Typed: Union types give us the possibility to add the types
of the responses from other actors to an actor’s Command
ADT!
Here’s how the Pinger
actor’s protocol looks now:
Pinger
and PingPong
actors with Union types appliedThe Pinger
actor’s implementation now looks as this:
object Pinger {
sealed trait Command
case object SendPing extends Command
case object StopPingPong extends Command
type CommandIncludingResponses = Command | PingPong.Response (1)
def apply(pingPong: ActorRef[PingPong.Ping]):
Behavior[CommandIncludingResponses] = (2)
Behaviors.setup { context =>
Behaviors.receiveMessage {
case StopPingPong =>
context.log.info(s"End of ping-pong game")
context.system.terminate()
Behaviors.stopped
case SendPing =>
pingPong ! PingPong.Ping(replyTo = context.self)
Behaviors.same
case response : PingPong.Response => (3)
context.log.info(s"Hey: I just received a $response !!!")
Behaviors.same
}
}
}
We introduce a type alias for convenience (➊). It is a Union of the original
Command
ADT and the type of the responses we want to be able process. We use
this Union type when we describe our Behavior
(➋). With this in place, we can
perform a pattern match of PingPong.Response
(➌) (which, in essence is a
Pong
message).
And that’s it! It doesn’t get more concise!
Conclusions
In this article we have shown two important things about Dotty:
-
We can use existing Scala 2.13 libraries as-is in combination with Dotty source code. This is a major convenience that can not be overestimated!
-
Dotty Union types can help to eliminate all the boilerplate that is needed to handle responses from other actors in a given actor when using Scala 2.
Based on what I’ve read and experimented with, it’s obvious that Dotty brings a lot of features that enable writing more concise and clearer code. To name just a few:
-
Top level definitions
-
Enums
-
The completely overhauled contextual abstractions system (aka implicits in Scala 2)
Stay tuned for more on this.
This is all very exciting and I can’t wait till the day Scala 3.0.0 hits Maven Central!
Article updates
-
2020-02-19: fix mismatch in message name of
Pinger
protocol