Akka Typed (with Scala)

  • Post author:
  • Post category:Akka / Scala

What is Akka?

Akka is a framework for writing message driven, scalable, error resilient applications, while making working with concurrency easy. This is achieved by modeling applications around the concept of “actors”.

Actors are entities that communicate with each other through messages. When an actor receives a message, it can react by

  • changing its internal state,
  • creating new actors, and
  • sending out messages to actors.

Multiple actors typically run in parallel, but the business logic within each actor is always executed single threaded, thereby avoiding complications that normally arise from concurrency.

Akka Typed

However, until the advent of Akka Typed, messages passed between actors have been untyped: You have been able to send any kind of message to any actor without the compiler being able to assert if the receiving actor is able to handle the message. This prevented detecting errors during compilation and made reasoning about actor driven applications difficult at times.

With Akka Typed, you can now specify which message types an actor is able to handle and the compiler will complain about mismatching message types sent to an actor.

An Example

The following is an example that shows the new Akka typed API in action (you can view and download the complete source here):

package dev.oreineke.akka.typed.example

import akka.actor.typed.ActorSystem
import akka.actor.typed.scaladsl.Behaviors

object Application extends App {
  // 1) define the message type "Message":
  sealed trait Message
  final case class Greet(name: String) extends Message
  final object Stop extends Message

  // 2) define the actor behavior (inluding the message type "Message" it can handle!):
  val readyToGreet = Behaviors.receive[Message] { (context, message) =>
    message match {
      case Greet(name) =>
        context.log.info(s"Hello $name!!!")
        Behaviors.same
      case Stop =>
        context.log.info("Goodbye!")
        Behaviors.stopped
    }
  }

  // 3) create the actor:
  val greeter = ActorSystem(readyToGreet, "Greeter")

  // 4) send a few messages to the actor:
  greeter ! Greet("Peter")
  greeter ! Greet("Paul")
  greeter ! Greet("Marry")
  greeter ! Stop

  // 5) the following would already fail during compilation
  //  greeter ! "Eat this"
}

Explanation

After defining the message type the actor is willing to accept in the Message trait,

  sealed trait Message
  final case class Greet(name: String) extends Message
  final object Stop extends Message

an actor Behavior named readyToGreet is defined:

  val readyToGreet = Behaviors.receive[Message] { (context, message) =>
    message match {
      case Greet(name) =>
        context.log.info(s"Hello $name!!!")
        Behaviors.same
      case Stop =>
        context.log.info("Goodbye!")
        Behaviors.stopped
    }
  }

Note that, to define the actor behavior readyToGreet, a typed factory method Behaviors.receive[Message] is used. It expects a function mapping the received message of type Message to a new Behavior. For the returned Behavior, instead of defining new behaviors, predefined factory methods are used to generate special return values: Behaviors.same to cause the current behavior to stay active, and Behavior.stop to cause the actor to die.

Finally, ActorSystem is called to create an actor with the Behavior readyToGreet (along with a name for easy actor identification):

  val greeter = ActorSystem(readyToGreet, "Greeter")

At this point, the actor greeter is ready to receive massages of type Message:

  greeter ! Greet("Peter")
  greeter ! Greet("Paul")
  greeter ! Greet("Marry")
  greeter ! Stop

If we would have tried to send a message of type String to the actor, the compiler would have noticed the error:

  greeter ! "Eat this" // this will fail during compilation