Repository navigation
StateAid
Text compliant with Leucine version 0.7.x
As stated before, per default all actors are stateless. Of course you can always store
state in a var and that is perfectly legal (as long as no mutable state leaks out
of the actor). This is still called stateless from the framework's point of view.
We have done this in the example above (for the device). But there is also a purely
functional way. If we want to manage state, a new mixin called StateAid is required.
This will be discussed in below with an example.
To show this we extend the Color Printer example
just a little and get rid of the var. Let's start the discussion with the companion object now.
This is defined as follows:
object Printer extends AcceptDefine :
/* These are the devices the printer is able to handle. */
enum Device :
case ANSI, XML, HTML
/* These are the colors the printer is able to handle. */
enum Colour(val ansi: Int, val style: String) :
case Red extends Colour(31,"red")
case Green extends Colour(32,"green")
case Blue extends Colour(34,"blue")
/* Format the text according to the parameters for output. */
def format(colour: Colour, device: Device, text: String): String = device match
case Device.ANSI => s"\u001B[${colour.ansi}m${text}\u001B[0m"
case Device.XML => s"<${colour.style}>${text}</${colour.style}>"
case Device.HTML => s"<span style=\"color:${colour.style};\">${text}</span>"
/* Make this class sealed so that the compile can check at the receiver method if
* we were complete in the implementation of all message types */
sealed trait Letter extends Actor.Letter[Actor]
/* Class to send a colored message to the printer. */
case class Message(colour: Colour, text: String) extends Letter
/* Message to dynamically switch the device of the messages. */
case class Switch(device: Device) extends Letter
/* Message to show the number of sheets printed. */
case object Prints extends Letter
/* The State holds two parameters, the number of prints until now
* and the current active device. */
case class State(device: Device, prints: Int) extends Actor.State :
/* Signal that we have printed a text. */
def printed: State = copy(prints = prints + 1)
/* Change the printer device */
def change(device: Device): State = copy(device = device)
/* Tell me how many prints were made. */
def show: String = s"Printed $prints times."
/* Start printing with an ANSI device, at zero prints. */
def initial: State = State(Device.ANSI,0)We have now defined a case class State which derives from Actor.State, both are required. It does not have to be
a case class, a trait or class will also do just fine, as long as it defines
the type State. Its base type must be Actor.State. It holds two fields which can
change as letters are handled. It is also obligatory to define a state to start with,
which may be called initial, and its value will be passed in the trait parameter in the StateAid mixin.
The State defined here has some convenience methods that make the transition from
one state to the next easier. And since we keep the current device in the state,
we may just as well define a method change to replace that device. Furthermore, we keep
track of the number of prints made.
Besides that, we have defined one new letter, which allows us to see how many prints
have been made so far. Printer.Prints as case object.
Now, have a look a the modified actor class:
class Printer extends AcceptActor(Printer,"printer"), StateAid[Printer.State](Printer.initial):
import Printer.*
/* Report that this logger has started. */
println("Started Printer Actor")
/* Receive method that handles the incoming printer and control messages, it must return the new state */
def receive(letter: Letter, sender: Sender): State = letter match
case Message(colour,text) => println(format(colour,state.device,text)); state.printed
case Switch(device) => state.change(device)
case Prints => println(state.show); state
case Prints => println(state.show); stateMost noticeable changes are the disappearance of the var device and the change
in return type of the receive method. It now expects the type State instead of
Unit. Alternatively you can use
Receive as return type (in both situations) and let the compiler do the work.
That would be
def receive(letter: Letter, sender: Sender): Receive = letter matchThe former state is available as background variable, and should only be used
when handling a letter via the receive method. You could argue it would be saver if this
was passed as parameter, and maybe this will be changed later.
Here it also implies of course that all match cases must return a value of the
type State. Implementation of the different situations is, with the convenience methods, straightforward.
Finally, lets put this to the test:
object Main :
import Printer.*
/* Define the printer */
val printer = new Printer
/* And make use of it like this: */
def main(args: Array[String]): Unit =
printer ! Message(Colour.Red,"This should be red.")
printer ! Message(Colour.Blue,"This should be blue.")
printer ! Switch(Device.XML)
printer ! Message(Colour.Green,"Now it should be xml green.")
printer ! Switch(Device.HTML)
printer ! Message(Colour.Red,"Red formatted as HTML.")
printer ! Prints
printer.stop(Actor.Stop.Finish)
ActorGuard.watch(false,1.second)Which results in:
Started Printer Actor
This should be red.
This should be blue.
<green>Now it should be xml green.</green>
<span style="color:red;">Red formatted as HTML.</span>
Printed 4 times.
This is probably what we would expect. First, the message that the Printer has been constructed, then the first message in "red", the second in blue. After changing the device, the next message is formatted as XML and subsequently as XML. Finally we ask for the number of prints, which should be 4.
You can also try this in Scastie.