Arguably, the major criticism made about akka is the fact that the receive function can accept anything and return nothing (
PartialFunction[Any, Unit]). It means that traditional Akka is effectively untyped. Akka-typed is intended to solve this particular problem by enforcing the types of incoming and outgoing messages. It does that by mandating us to define a new kind of data: Behaviors. But what are behaviors good for? We can think of behaviors as our communication protocol. In fact, with akka-typed, we are in charge of the protocol and actors do the actual work under the hood. This article assumes that you have at least a basic level of familiarity with Akka and the Scala programming language.
Hello actor! It is time to do the mandatory
Hello world application. First, let’s see the traditional akka-untyped approach:
It is time for akka-typed. As mentioned before, we need to build Behaviors. In its simplest form, a Behavior requires the following elements:
Wait, what? No
receive function? No
Actor trait? Worry not!, we will guide you through this new way of implementing your actors with a more complete example, prime calculation. Remember that all the source code is available in the repository for this article.
Eratosthenes prime calculation
The Sieve of Eratosthenes is an algorithm for calculating prime numbers. It goes as follows:
- Create a list, l of the consecutive numbers from 2 to n
- Assume the first number is a prime
- Take the first number of the list, h
- Remove from l the numbers that are multiples of h
- Add h to the result
- Back to step 2.
Using an actor system, we choose to design it using the following actors and message exchanges:
This actor is responsible for starting the prime calculation. We can think of it as a façade to the whole system. This actor does the following:
- Creates an instance of the
- Sends the
FindPrimesmessage containing the upper limit in our calculation
The akka-typed way of creating actors is based on the spawn function added to the
ActorContext through the implicit class
UntypedActorContextOps present in
akka.typed.scaladsl.adapter. Now, we get a typed
ActorRef! So, in our example, master is actually an
ActorRef[MasterProtocol]. It is important to notice that the concept of props is gone. With akka-typed, only one function containing the behavior is enough to create an actor.
Not much changed here.
This actor is our first truly typed actor. It is responsible for:
Let’s define our protocol accordingly:
Before getting into the details, it is necessary to clearly identify the elements of the akka-typed Domain Specific Language (DSL) we are going to handle. First, we need to keep in mind that protocols are usually defined as sealed traits (also known as a Sum Types or sealed family) because the compiler can warn us when we are mistakenly sending and receiving messages that do not belong to the protocol. Our typed actor needs a function to construct its behavior using the protocol we created. It just means we need to provide a function returning a
To build the behavior, the akka-typed DSL provides us with the
Actor.deferred function which is defined as
ActorContext[T] => Behavior[T]. We can think of it as a replacement of the code we used to have in
preStart(...) or even in our actor’s constructor body. As such, it is useful in operations that involve the context previous to any message (e.g. creating actors, scheduling calls, etc).
To handle messages there is another useful function called
Actor.immutable. It is defined as
(ActorContext[T], T) => Behavior[T]). It implies that in akka-typed we don’t receive only our message, but a tuple containing the context and our actual message. We can think of this function as the replacement of the previous
receive: Receive function. It is time to actually handle the messages of our protocol using
Actor.immutable. As it was stated before, we need to handle:
FindPrimes, message sent from the
PrimeFinderactor to start the calculation. We just need to calculate the list of natural numbers until the upper limit and send them to the Eratosthenes actor. Notice how the state needs to change because now there is a reference to the actor that accepts the reply.
Result, message sent from the Eratosthenes actor when the recursion is done.
As the state is not changing, we need to inform the system that there is no problem in reusing the previous state. The function
Actor.same does exactly that.
One alternative could be as follows:
Notice how there is no
unbecome. All that is needed a call to
handleMessagesAndReplyTo with the actual
actorRef that accepts the result. Assembling the pieces together gives this code:
As usual, we first need to define our protocol. In this case it is quite easy since we only need to handle a single
Sieve message containing:
- A reference to the Master actor
- The current list of prime numbers
- The remaining numbers to handle
OK, let’s implement our behavior. Since there’s no need to do anything before receiving the actual message, then our behavior goes in a
Actor.immutable. For this actor, we can handle messages similar to what we did with Master. However, we cannot ignore the first element in the tuple
(ActorContext[T], ActualMessage) because we will need context to send us back the
Sieve message to continue with the next iteration. Then, because the state doesn’t change, we just return the same state through
Now that we have all the assembling pieces, the whole code for this actor goes like this:
We covered an example showing the main changes coming with akka-typed. We explained how to interact with untyped actors and how to create Behaviors. As you might have noticed, akka-typed brings fundamental changes to our actors. However, as we are forced to create and respect our protocol, we are pretty sure that the compiler will warn us in case we are not respecting the Types. We think this new version goes in the good direction: both to enforce types and to simplify our own implementations. We hope you enjoyed. See you next time.