project gålbma – actors vs types
TRANSCRIPT
Motivation
3
case class Getcase class Got(contents: Map[String, ActorRef])
class Server extends Actor { var map = Map.empty[String, ActorRef] def receive = { case Get => sender ! Got(map) }}
4
case class GetRef(name: String)case class GetRefReply(ref: Option[ActorRef])
class Client(server: ActorRef) extends Actor { def receive = { case GetRef(name) => val worker = context.actorOf(Worker.props(name, sender())) server.tell(Get, worker) }}
object Worker { def props(name: String, replyTo: ActorRef) = Props(new Worker(name, replyTo))}
class Worker(name: String, replyTo: ActorRef) extends Actor { def receive = { case Got(map) => replyTo ! GetRefReply(map.get(name)) context.stop(self) }}
5
case class Get(id: Int)case class Got(id: Int, contents: Map[String, ActorRef])
class Server extends Actor { var map = Map.empty[String, ActorRef] def receive = { case Get(id) => sender ! Got(id, map) }}
6
case class GetRef(name: String)case class GetRefReply(ref: Option[ActorRef])
class Client(server: ActorRef) extends Actor { def receive = { case GetRef(name) => val worker = context.actorOf(Worker.props(name, sender())) server.tell(Get, worker) }}
object Worker { def props(name: String, replyTo: ActorRef) = Props(new Worker(name, replyTo))}
class Worker(name: String, replyTo: ActorRef) extends Actor { def receive = { case Got(id, map) => replyTo ! GetRefReply(map.get(name)) context.stop(self) }}
7
class Asker(server: ActorRef) extends Actor { implicit val timeout = Timeout(1.second) import context.dispatcher
def receive = { case GetRef(name) => (server ? Get(42)) .mapTo[Got] .map(got => GetRefReply(got.contents get name)) .pipeTo(sender()) }}
Akka 1.2: Channel[-T]
9
/** * Abstraction for unification of sender and senderFuture for later reply. * Can be stored away and used at a later point in time. * * The possible reply channel which can be passed into ! and tryTell is always * untyped, as there is no way to utilize its real static type without * requiring runtime-costly manifests. */trait Channel[-T] extends japi.Channel[T] {
/** * Scala API. <p/> * Sends the specified message to the channel. */ def !(msg: T)(implicit sender: UntypedChannel): Unit
...}
The Failures Summarized
• first no clear vision of the goal
• then trying to go too far • too complicated to declare
• white-box macros required
• not bold enough • untyped Actors have features that are incompatible with
static typing
13
What we want: Parameterized ActorRef
15
object Server { case class Get(id: Int)(val replyTo: ActorRef[Got]) case class Got(id: Int, contents: Map[String, ActorRef[OtherCommand]])}
object Client { case class GetRef(name: String)(val replyTo: ActorRef[GetRefReply]) case class GetRefReply(ref: Option[ActorRef[OtherCommand]])}
val server: ActorRef[Server.Get] = ???
val behavior: PartialFunction[Any, Unit] = { case g @ GetRef(name) => (server ? Server.Get(42)) .map(got => g.replyTo ! GetRefReply(got.contents get name))}
What we want: Parameterized ActorRef
16
object Server { case class Get(id: Int)(val replyTo: ActorRef[Got]) case class Got(id: Int, contents: Map[String, ActorRef[OtherCommand]])}
object Client { case class GetRef(name: String)(val replyTo: ActorRef[GetRefReply]) case class GetRefReply(ref: Option[ActorRef[OtherCommand]])}
val server: ActorRef[Server.Get] = ???
val behavior: PartialFunction[Any, Unit] = { case g @ GetRef(name) => (server ? Server.Get(42)) .map(got => g.replyTo ! GetRefReply(got.contents get name))}
What we want: Parameterized ActorRef
17
object Server { case class Get(id: Int)(val replyTo: ActorRef[Got]) case class Got(id: Int, contents: Map[String, ActorRef[OtherCommand]])}
object Client { case class GetRef(name: String)(val replyTo: ActorRef[GetRefReply]) case class GetRefReply(ref: Option[ActorRef[OtherCommand]])}
val server: ActorRef[Server.Get] = ???
val behavior: PartialFunction[Any, Unit] = { case g @ GetRef(name) => (server ? Server.Get(42)) .map(got => g.replyTo ! GetRefReply(got.contents get name))}
The Guiding Principle
• build everything around ActorRef[-T]
• do not use macros or type calculations that Java cannot do (i.e. “keep it simple”)
• remove all features that are incompatible with this • in particular the automatic “sender” capture must go
18
Possible Plan
• add type parameter to ActorRef, Actor, …
• remove sender() • type Receive = PartialFunction[T, Unit]
• restrict context.become to this type
• type-safety achieved—everyone happy!
19
Project Gålbma
• distill an Actor to its essence: the Behavior
• everything is a message—for real this time
• remove the danger to close over Actor environment
• behavior composition
• allow completely pure formulation of Actors
23
Behavior is King, no more Actor trait
24
object Server { sealed trait Command case class Get(id: Int)(val replyTo: ActorRef[Got]) extends Command case class Put(name: String, ref: ActorRef[OtherCommand]) extends Command
case class Got(id: Int, contents: Map[String, ActorRef[OtherCommand]])
val initial: Behavior[Command] = withMap(Map.empty)
private def withMap(map: Map[String, ActorRef[OtherCommand]]) = Total[Command] { case g @ Get(id) => g.replyTo ! Got(id, Map.empty) Same case Put(name, ref) => withMap(map.updated(name, ref)) }}
No More Closing over ActorContext
• ActorContext is passed in for every message
• processing a message returns the next behavior
• lifecycle hooks, Terminated and ReceiveTimeout are management “signals”
25
final case class Total[T](behavior: T => Behavior[T]) extends Behavior[T] { override def management(ctx: ActorContext[T], msg: Signal): Behavior[T] = Unhandled override def message(ctx: ActorContext[T], msg: T): Behavior[T] = behavior(msg) override def toString = s"Total(${LineNumbers(behavior)})"}
Everything behaves like a Message
• ActorContext remains the system interface: • spawn, stop, watch, unwatch, setReceiveTimeout, schedule,
executionContext, spawnAdapter, props, system, self
• actorOf — for interoperability with untyped Actors
26
Full[Command] { case Msg(ctx, cmd) => // def receive case Sig(ctx, PreStart) => // def preStart() case Sig(ctx, PreRestart(ex)) => // def preRestart(...) case Sig(ctx, PostRestart(ex)) => // def postRestart(...) case Sig(ctx, PostStop) => // def postStop() case Sig(ctx, Failed(ex, child)) => // val supervisorStrategy case Sig(ctx, ReceiveTimeout) => // case ReceiveTimeout case Sig(ctx, Terminated(ref)) => // case Terminated(...) }
27
object Client { sealed trait Command case class GetRef(name: String)(val replyTo: ActorRef[GetRefReply]) extends Command case class GotWrapper(id: Int, contents: Map[String, ActorRef[OtherCommand]]) extends Command
case class GetRefReply(ref: Option[ActorRef[OtherCommand]])
def initial(server: ActorRef[Server.Command]) = ContextAware[Command] { ctx => val adapter = ctx.spawnAdapter((got: Server.Got) => GotWrapper(got.id, got.contents)) behv(0, Map.empty)(adapter, server) }
def behv(nextId: Int, replies: Map[Int, (String, ActorRef[GetRefReply])])( implicit adapter: ActorRef[Server.Got], server: ActorRef[Server.Command]): Behavior[Command] = Total { case g @ GetRef(name) => server ! Server.Get(nextId)(adapter) behv(nextId + 1, replies.updated(nextId, name -> g.replyTo)) case GotWrapper(id, contents) => replies get id map (p => p._2 ! GetRefReply(contents get p._1)) behv(nextId, replies - id) }}
28
object Client { sealed trait Command case class GetRef(name: String)(val replyTo: ActorRef[GetRefReply]) extends Command case class GotWrapper(id: Int, contents: Map[String, ActorRef[OtherCommand]]) extends Command
case class GetRefReply(ref: Option[ActorRef[OtherCommand]])
def initial(server: ActorRef[Server.Command]) = ContextAware[Command] { ctx => val adapter: ActorRef[Server.Got] = ctx.spawnAdapter((got: Server.Got) => GotWrapper(got.id, got.contents)) behv(0, Map.empty)(adapter, server) }
def behv(nextId: Int, replies: Map[Int, (String, ActorRef[GetRefReply])])( implicit adapter: ActorRef[Server.Got], server: ActorRef[Server.Command]): Behavior[Command] = Total { case g @ GetRef(name) => server ! Server.Get(nextId)(adapter) behv(nextId + 1, replies.updated(nextId, name -> g.replyTo)) case GotWrapper(id, contents) => replies get id map (p => p._2 ! GetRefReply(contents get p._1)) behv(nextId, replies - id) }}
29
object Client { sealed trait Command case class GetRef(name: String)(val replyTo: ActorRef[GetRefReply]) extends Command case class GotWrapper(id: Int, contents: Map[String, ActorRef[OtherCommand]]) extends Command
case class GetRefReply(ref: Option[ActorRef[OtherCommand]])
def initial(server: ActorRef[Server.Command]) = ContextAware[Command] { ctx => val adapter = ctx.spawnAdapter((got: Server.Got) => GotWrapper(got.id, got.contents)) behv(0, Map.empty)(adapter, server) }
def behv(nextId: Int, replies: Map[Int, (String, ActorRef[GetRefReply])] )(implicit adapter: ActorRef[Server.Got], server: ActorRef[Server.Command]): Behavior[Command] = Total { case g @ GetRef(name) => server ! Server.Get(nextId)(adapter) behv(nextId + 1, replies.updated(nextId, name -> g.replyTo)) case GotWrapper(id, contents) => replies get id map (p => p._2 ! GetRefReply(contents get p._1)) behv(nextId, replies - id) }}
The Implementation
• independent add-on library
• layered completely on top of untyped Actors • currently 2kLOC main + 1.7kLOC tests
• fully interoperable
31
The most important interface: Behavior[T]
• Behaviors: • Full, FullTotal, Total, Partial, Static
• Decorators: • ContextAware, SelfAware, SynchronousSelf, Tap
• Combinators: • And, Or, Widened
32
abstract class Behavior[T] { def management(ctx: ActorContext[T], msg: Signal): Behavior[T] def message(ctx: ActorContext[T], msg: T): Behavior[T] def narrow[U <: T]: Behavior[U] = this.asInstanceOf[Behavior[U]]}
ActorSystem ≈ ActorRef
33
object Demo extends App { implicit val t = Timeout(1.second)
val guardian = ContextAware[Client.Command] { ctx => val server = ctx.spawn(Props(Server.initial), "server") val client = ctx.spawn(Props(Client.initial(server)), "client") Static { case msg => client ! msg } }
val system = ActorSystem("Demo", Props(guardian)) import system.executionContext
system ? Client.GetRef("X") map println foreach (_ => system.terminate())}
Behavior Rulez!
• decoupling of logic from execution mechanism
• synchronous behavioral tests of individual Actors
• mock ActorContext allows inspection of effects
35
36
object `A Receptionist` {
def `must register a service`(): Unit = { val ctx = new EffectfulActorContext("register", Props(behavior), system) val a = Inbox.sync[ServiceA]("a") val r = Inbox.sync[Registered[_]]("r")
ctx.run(Register(ServiceKeyA, a.ref)(r.ref)) ctx.getAllEffects() should be(Effect.Watched(a.ref) :: Nil) r.receiveMsg() should be(Registered(ServiceKeyA, a.ref))
val q = Inbox.sync[Listing[ServiceA]]("q")
ctx.run(Find(ServiceKeyA)(q.ref)) ctx.getAllEffects() should be(Nil) q.receiveMsg() should be(Listing(ServiceKeyA, Set(a.ref)))
assertEmpty(a, r, q) }
...}
Encoding Types with Members
38
class MyClass {
def myMethod(id: Int): String def otherMethod(name: String): Unit protected def helper(arg: Double): Unit
}
Encoding Types with Members
• Typed Actors provide complete modules with members
• Typed Actors can encode more flexible access privileges
• more verbose due to syntax being optimized for classes
39
object MyClass { sealed trait AllCommand sealed trait Command extends AllCommand case class MyMethod(id: Int)(replyTo: ActorRef[String]) extends Command case class OtherMethod(name: String) extends Command case class Helper(arg: Double) extends AllCommand
val behavior: Behavior[Command] = behavior(42).narrow private def behavior(x: Int): Behavior[AllCommand] = ???}
Calling Methods
40
object MyClassDemo { import MyClass._ val myClass: MyClass = ??? val myActor: ActorRef[Command] = ??? implicit val t: Timeout = ???
myClass.otherMethod("John") myActor!OtherMethod("John")
val result = myClass.myMethod(42) val future = myActor?MyMethod(42)}
But Actors can do more: Protocols
41
object Protocol { case class GetSession(replyTo: ActorRef[GetSessionResult])
sealed trait GetSessionResult case class ActiveSession(service: ActorRef[SessionCommand]) extends GetSessionResult with AuthenticateResult case class NewSession(auth: ActorRef[Authenticate]) extends GetSessionResult
case class Authenticate(username: String, password: String, replyTo: ActorRef[AuthenticateResult])
sealed trait AuthenticateResult case object FailedSession extends AuthenticateResult
trait SessionCommand}
What can we express?
• everything a classical module with methods can
• pass object references as inputs and outputs
• patterns beyond request–response
• dynamic proxying / delegation
43
What can we NOT express?
• any dynamic behavior (e.g. internal state changes)
• session invalidation
44
Current Status
• part of Akka 2.4-M1 • http://doc.akka.io/docs/akka/2.4-M1/scala/typed.html
• only bare Actors • no persistence
• no stash
• no at-least-once delivery
• no Java API yet (but taken into account already)
46
Next Steps
• proper Java API (probably in 2.4-M2)
• Receptionist plus akka-distributed-data for Cluster
• port Actor-based APIs to typed ones (e.g. Akka IO)
• add FSM support with transition triggers
• completely pure Actor implementation,«Actor Action Monad» (inspired by Join Calculus)
• listen to community feedback
47
… and in the far future:
• reap internal benefits by inverting implementation: • remove sender field (and thus Envelope)
• make untyped Actor a DSL layer on top of Akka Typed
• declare it non-experimental
48