First example

First, we need to define the protocol that our Oven will speak. In contrast to the untyped implementation, we can't reuse messages that are defined by another actor. The reason for this is that the Oven (and other actors at later stages) defines the type of messages it is supposed to handle. This type should not be too general in order to avoid making the whole implementation less typed than desirable.

The domain model is common for all actors, so we'll define it on the Bakery app level:

final case class Groceries(eggs: Int, flour: Int, sugar: Int, chocolate: Int)
final case class Dough(weight: Int)
final case class RawCookies(count: Int)
final case class ReadyCookies(count: Int)

And this is the small language our Oven speaks:

sealed trait Command
case class Put(rawCookies: Int, sender: ActorRef[Baker.Command]) extends Command
case class Extract(sender: ActorRef[Baker.Command]) extends Command

The Oven can return ReadyCookies (a cookie is considered to be ready as soon as it has been put into the oven) and RawCookies in the case where there are more cookies in the Put command than can fit inside the oven. The Command is a type of behavior for our actor. We can see that it includes the sender field so that the oven knows who is the receiver of the extracted cookies.

Now, we need to define the actor's behavior. If you followed the previous chapter, you will remember that we used the internal mutable field to store the contents of the oven in the current moment. By using this field, we can differentiate its reaction on incoming messages. Akka Typed urges us to exercise a different approach and use separate behaviors for different states of the actor. First, we define what should happen in the case that there is nothing inside:

def empty: Behaviors.Receive[Command] = Behaviors.receiveMessage[Command] {
case Put(rawCookies, sender) =>
val (inside, overflow: Option[RawCookies]) = insert(rawCookies)
overflow.foreach(sender.tell)
full(inside)
}

Here, we define a behavior of the empty Oven using the Behaviors factory. In our case, this is a receiveMessage method with a type parameter called Command. This designates the type of messages our actor can handle.

Next, we define a course of action in the case of the incoming Put command. The insert method returns a number of cookies that we can put inside the Oven and an optional overflow. In this case, if there is an overflow, we return it to the sender by using the tell method of its ActorRef[Cookies]. The type of reference allows us to send RawCookies back. Because of the type-safe nature of the actor definition, this binds the behavior of the Baker actor (that we'll implement soon) to be Behaviors.Receive[Cookies]

Now, we need to define what should happen in the case that the Oven is not empty:

def full(count: Int): Behaviors.Receive[Command] = Behaviors.receiveMessage[Command] {
case Extract(sender) =>
sender ! ReadyCookies(count)
empty
}

This Behavior is even simpler, but still has the same type—Behaviors.Receive[Command]. We just return all of the cookies that were inside to the sender and change the future behavior to the empty behavior we defined earlier. 

Now, if we compile this implementation, we'll get complaints from the compiler:

Warning:(18, 77) match may not be exhaustive.
It would fail on the following input: Extract(_)
def empty: Behaviors.Receive[Command] = Behaviors.receiveMessage[Command] {
Warning:(25, 88) match may not be exhaustive.
It would fail on the following input: Put(_, _)
def full(count: Int): Behaviors.Receive[Command] = Behaviors.receiveMessage[Command] {

The compiler has helped us to identify our first two bugs already! The reason it is unhappy with the current implementation is that we forgot to define a reaction to the messages that are inappropriate in specific states. This will be an attempt to extract cookies from the empty oven and to put something into the full one. From the type perspective, this is possible, and the compiler informed us of this.

Let's fix this by implementing our states properly:

This is the augmented definition of an empty state:

def empty: Behaviors.Receive[Command] = Behaviors.receiveMessage[Command] {
case Put(rawCookies, sender) =>
val (inside, tooMuch) = insert(rawCookies)
tooMuch.foreach(sender.tell)
full(inside)
case Extract(sender) =>
sender ! ReadyCookies(0)
Behaviors.same
}

The sender will be sent zero cookies, and we keep current behavior by using Behavior.same.

The principle stays the same for the full case:

def full(count: Int): Behaviors.Receive[Command] = Behaviors.receiveMessage[Command] {
case Extract(sender) =>
sender ! ReadyCookies(count)
empty
case Put(rawCookies, sender) =>
sender ! RawCookies(rawCookies)
Behaviors.same
}

Again, we just returned to the sender everything we've got and kept the current behavior exactly like we did in the empty case.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.227.26.217