Categories
Computing

The return of the typed Actor

People regularly complains about the lack of type safety within Akka actors. After 2 rather unsuccessful attempts (using byte-code generation at runtime for the first and java proxies for the second) the third attempt seems much more promising.

Let’s start with regular non-typed actors to implement a very basic toy example and then move on towards a fully typed actor.

There is one actor that receives a Hello message, print it to the console and replies with a NiceToMeetYou message.

Here are the message definition:

object UntypedActor {
   case class Hello(from: String)
   case class NiceToMeetYou(from: String)
}

And the code of the actor:

class UntypedActor extends Actor {
   import UntypedActor._

   def receive: Receive = {
      case Hello(from) =>
         println(s" $from > 'Hello'")
         sender() ! NiceToMeetYou(from = "Actor")
   }
}

Now let’s create a small app to create an actor, send a Hello message to it and print the response.

object UntypedApp {

  def main(args: Array[String]) = {
    import UntypedActor._
    val system = ActorSystem("UntypedActorSystem")
    implicit val executionContext = system.dispatcher
    implicit val timeout = Timeout(1 second)

    val actor = system.actorOf(Props[UntypedActor])

    (actor ? Hello(from = "Main"))
      .mapTo[NiceToMeetYou]
      .foreach{
        case NiceToMeetYou(from) =>
          println(s" $from > 'Nice to meet you'")
        }
  }

}

If you run this program you’ll see the following output on the console:

Main > 'Hello'
Actor > 'Nice to meet you'

This is the actor implementation we’re all used to. That is we can send any message to any actor. And in case of the ask pattern it returns a Future[Any] that we have to map to the correct type with mapTo[CorrectType]. This mapTo operation is just a runtime cast. We can put whatever type we want in this operation there is no guarantee that this is the type sent back by the actor.

If we want to solve this we need to use typed ActorRefs. That mean that our actor should be of type ActorRef[Hello] has it handles Hello messages. Similarly we’ll get back a Future[NiceToMeetYou] without having to cast it to the correct type.

However there is one major problem with this approach. It’s the sender() method available in the Actor itself. We use this method to send back a NiceToMeetYou reply. In this case we want sender() to give us back an ActorRef[NiceToMeetYou] but it gives us an untyped ActorRef (or ActorRef[Any]) because there is no way for the method to know that the actor is going to send back a NiceToMeetYou message to the sender.

The simplest way to solve this is to pass an ActorRef[NiceToMeetYou] into the Hello message so that the actor knows where to send the replies without using the sender() method.

Using this approach our actor becomes:

object TypedActor {

  case class Hello(from: String, sender: ActorRef[NiceToMeetYou])
  case class NiceToMeetYou(from: String)

  val actor = Static[Hello] { msg =>
    println(s" ${msg.from} > 'Hello'")
    msg.sender ! NiceToMeetYou(from = "Actor")
  }

}

The Hello message now contains the actorRef where to send the NiceToMeetYou reply.
And the application code becomes

object App {

  import TypedActor._

  def main(args: Array[String]): Unit = {
    val actor = ActorSystem("Hello", Props(actor))

    implicit val ec = actor.dispatchers.defaultGlobalDispatcher
    implicit val timeout: Timeout = 1 second

    val response: Future[NiceToMeetYou] = actor ? (Hello(from = "Main", _))

    response
      .map(msg => println(s" ${msg.from} > 'Nice to meet you'"))
      .recover { case e => e.getMessage }
      .flatMap(_ => actor.terminate())
  }

}

As you can see the mapTo operation is now gone. But there’s more than that!

You’d probably blinked a couple of times when you saw the actor implementation. Our actor definition no longer extends the Actor trait. It is now just a simple function Hello => Unit.

That’s the magic of the typed actor. Now that the sender method is gone, so can go away everything from the Actor trait.

Wait a minute, what about the lifecycle hooks ? Are they gone as well ?
Obviously they’re no longer here – but fear not – they haven’t disappear. There are now sent to the actor as messages or more exactly as Signals. And that’s even better because having specific messages for lifecycle allows to close over variables even during lifecycle (yes, no more mutable instance variables!)

The nice thing with all this refactoring is that it boils down to the essence of actors. That is an actor can do 3 things upon reception of a message:

  • Create new actors
  • Send messages to actors he knows
  • Change behaviour

And that’s all … nothing more. So let’s have a look at changing behaviour but let’s start by examining what is a behaviour for a typed actor.

We’ve already seen that it’s a function from MessageType => Unit but more exactly it’s a function from MessageType => Behaviour.

The function returns the new behaviour to apply to the next message and this what that Static construct does in

Static[Hello] { msg =>
  println(s" ${msg.from} > 'Hello'")
  msg.sender ! NiceToMeetYou(from = "Actor")
}

Here, Static takes a function Hello => Unit and always returns the same behaviour for the next message.

In fact a behaviour an abstract class that provides 2 methods message() and management()

abstract class Behavior[T] {
   def management(ctx: ActorContext[T], msg: Signal): Behavior[T]
   def message(ctx: ActorContext[T], msg: T): Behavior[T]
   ...
}

Message is intended to handle user defined messages and management is intended to handle Signals which replace the lifecycle hooks.

An interesting thing to note as well is that the ActorContext is now passed in as a function parameter to allow interacting with the actor context (like creating new actors) and is no longer an instance variable.

Fortunately Akka provides several behaviour constructs to support different use cases:

  • Full
  • FullTotal
  • Total
  • Partial
  • ContextAware
  • SelfAware
  • Static

I encourage you to have a look at the source code on github to see the differences between them (don’t worry the source code is well docummented).

Since a behaviour is just a function is gives us some nice properties:

  • it simplifies testing as behaviour are now decoupled from the execution logic. Actors behaviours can be tested in isolation without instantiating and instrumenting a whole actor system. For instance by mocking the actor context we can have a fully synchronous way to test actors behaviours.
  • it improves composition. People used to complain that actors don’t compose. But now an actor is just a function and functions do compose.

Back to our basic example you may have notice that the way to create the actor system has also changed. We create our actor system but we also passed some Props in and got back an ActorRef. What the h…?

Well there is a reason behind all of these … and it’s called actorOf. Previously we could create any top level actor by calling system.actorOf. This lead to complex management of actor that were not child of any other actor.

The typed actor refactoring was the chance to get rid of this complexity and remove the system.actorOf method. Good, but we still need one top level actor – the guardian actor of the system. And we obtain this actor once when we create the actor system. That’s why we need to pass in some Props. And if you look at the new ActorSystem declaration you’ll notice that we get back something that is both an ActorSystem and an ActorRef

abstract class ActorSystem[-T](_name: String) extends ActorRef[T] {
   ...
}

So all the actors we can create are now children of the single top-level guardian actor.