Skip to content

StateAid

devlaam edited this page Oct 19, 2026 · 1 revision

StateAid: Actors with managed state

Text compliant with Leucine version 0.7.x

Introduction

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.

Example: The Printer with memory.

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); state

Most 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 match

The 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.

Internals of an actor

Clone this wiki locally