Skip to content

WideActor

devlaam edited this page May 23, 2023 · 9 revisions

WideActor: Open to the world

Introduction

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.

Ping Pong with Akka.

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.

Ping Pong with Leucine

The steps we have to take to translate this example to Leucine are:

  1. Derive all letters from Actor.Letter[Actor]
  2. Define companion objects for each actor.
  3. Define states for the different Receive partial functions.
  4. Derive all actors from WideActor

Derive all letters from Actor.Letter[Actor]

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]

Define companion objects for each 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.

Define states for the different partial functions.

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 }

Derive all actors from WideActor

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

Main method

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

$\textcolor{red}{\mbox{\scriptsize{}TO BE COMPLETED}}$

Internals of an actor

Clone this wiki locally