-
Notifications
You must be signed in to change notification settings - Fork 1
WideActor
The WideActor
is an actor that effectively eliminates the need
for typed letters. By effectively we mean that, although the actor
is not able to receive an instance of the type Any
it is able to
directly receive any instance of the type Actor.Letter[Actor]
. Since
you are always able to derive any letter from this type, it
works as the 'Any' among the letters.
Now, to which end is this a good idea? Is it not much saver to have the letters, and where possible the senders as well, typed? Yes it is, but if you are moving from one actor system to an other, it might be good to do so with a running system. Transforming every letter from untyped to typed requires quite some refactoring, which you might not want to do all at once. So step by step is better.
Let's show this, starting from an example.
Suppose we have the following example Ping Ping example in Scala 3. This particular example was based on an example generated by ChatGPT 4.0, but it resembles the ping-pong example by Alvin Alexander a lot, so credit goes to him as well.
import akka.actor.{Actor, ActorSystem, Props}
object Letters :
case object Ping
case object Pong
case object Start
case object Stop
class PingActor extends Actor :
def receive: Receive =
case Letters.Start =>
println("Ping received Start")
sender() ! Letters.Ping
context.become(waitForPong)
def waitForPong: Receive =
case Letters.Pong =>
println("Ping received Pong")
sender() ! Letters.Ping
context.become(waitForStop)
def waitForStop: Receive =
case Letters.Stop =>
println("Ping received Stop")
context.stop(self)
class PongActor extends Actor :
def receive: Receive =
case Letters.Ping =>
println("Pong received Ping")
sender() ! Letters.Pong
case Letters.Stop =>
println("Pong received Stop")
context.stop(self)
object Main :
val system = ActorSystem("PingPongSystem")
val pingActor = system.actorOf(Props[PingActor]())
val pongActor = system.actorOf(Props[PongActor]())
def main(args: Array[String]): Unit =
pingActor.tell(Letters.Start,pongActor)
Thread.sleep(200)
pingActor ! Letters.Stop
pongActor ! Letters.Stop
Thread.sleep(200)
system.terminate()
The main method activates the ping pong sequence
by sending Start
to the PingActor
, as if the
PongActor
did so. PingActor
answers PongActor
with Ping
and changes
its state to accept Pong
only. After the Pong
comes
in from the PongActor
, the PingActor
changes it state again to wait for Stop
.
You can see the code in action in Scastie.
What we want to do now, is to transform this example into something that compiles under Leucine. The transformation is important here, for writing an application with the same goal is probably easier.
The steps we have to take to translate this example to Leucine are:
- Derive all letters from
Actor.Letter[Actor]
- Define companion objects for each actor.
- Define states for the different Receive partial functions.
- Derive all actors from
WideActor
This is pretty strait forward, we get:
object Letters :
case object Ping extends Actor.Letter[Actor]
case object Pong extends Actor.Letter[Actor]
case object Start extends Actor.Letter[Actor]
case object Stop extends Actor.Letter[Actor]
For the PongActor
this is extremely short, we just need to state
that we are stateless. There are no other types to define
since the Letters are all defined in one object.
object PongActor extends WideDefine, Stateless
For PingActor
this is a little more involved since we utilize
the State
here. See below.
In Leucine the receive
method is an ordinary method and not a partial
function. The concept of become
is not present. So how do we go about?
One solution is to ignore this difference and simply put all the handling
in one method for PingActor
, just as is done in PongActor
. In this case
that would work, but we must also be prepared for the general case.
We solve this to define enum's on the state, so we get:
object Ping extends WideDefine :
enum State { case WaitForStart, WaitForPong, WaitForStop }
Again, for PongActor
this is the most easy
class PongActor extends WideActor(PongActor) :
def receive(letter: Letter, sender: Sender): Receive = letter match
case (Letters.Ping, source: PingActor) =>
println("Pong received Ping")
source ! Letters.Pong
case (Letters.Stop,_) =>
println("Pong received Stop")
stop(Actor.Stop.Direct)
For PingActor
we handle each state in combination with the expected message. When received
we have a state transition to the new state. Note that an unhandled letter here does
not lead to a dead letter, but to a run time error. If you do not want that, the
user must explicitly handle the situation with a catch all. Here you see the disadvantage
from accepting letters from any actor. The compiler cannot warn you for unhandled situations.
class PingActor extends WideActor(PingActor) :
def receive(letter: Letter, sender: Sender, state: State): Receive = (state: State) => (letter,sender,state) match
case (Letters.Start, source: PongActor, State.WaitForStart) =>
println("Ping received Start")
source ! Letters.Ping
State.WaitForPong
case (Letters.Pong, source: PongActor, State.WaitForPong) =>
println("Ping received Pong")
source ! Letters.Ping
State.WaitForStop
case (Letters.Stop, _, State.WaitForStop) =>
println("Ping received Stop")
stop(Actor.Stop.Direct)
State.WaitForStart
The main method becomes slightly simpler, with the same functionality. New actors
can simply be defined, no further trickery necessary. With send
we can send
a letter in the name of some other actor. Finally, there is no need to manually
terminate the system; when there are no letters to process left, the system terminates
itself.
object Main :
val pingActor = new PingActor
val pongActor = new PongActor
def main(args: Array[String]): Unit =
pingActor.send(Letters.Start,pongActor)
Thread.sleep(200)
pingActor ! Letters.Stop
pongActor ! Letters.Stop