Project Gålbma:
Actors vs Types
Dr. Roland Kuhn
@rolandkuhn — Akka Tech Lead
Motivation
Motivation
3
case class Get
case class Got(contents: Map[String, ActorRef])
class Server extends Actor {
var map = Map.emp...
4
case class GetRef(name: String)
case class GetRefReply(ref: Option[ActorRef])
class Client(server: ActorRef) extends Act...
5
case class Get(id: Int)
case class Got(id: Int, contents: Map[String, ActorRef])
class Server extends Actor {
var map = ...
6
case class GetRef(name: String)
case class GetRefReply(ref: Option[ActorRef])
class Client(server: ActorRef) extends Act...
7
class Asker(server: ActorRef) extends Actor {
implicit val timeout = Timeout(1.second)
import context.dispatcher
def rec...
Failed Attempts
Akka 1.2: Channel[-T]
9
/**
* Abstraction for unification of sender and senderFuture for later reply.
* Can be stored away...
Akka 2.1: Typed Channels
10
Akka 2.1: Typed Channels
11
Akka 2.1: Typed Channels
12
The Failures Summarized
• first no clear vision of the goal
• then trying to go too far
• too complicated to declare
• whi...
The Solution
What we want: Parameterized ActorRef
15
object Server {
case class Get(id: Int)(val replyTo: ActorRef[Got])
case class Got...
What we want: Parameterized ActorRef
16
object Server {
case class Get(id: Int)(val replyTo: ActorRef[Got])
case class Got...
What we want: Parameterized ActorRef
17
object Server {
case class Get(id: Int)(val replyTo: ActorRef[Got])
case class Got...
The Guiding Principle
• build everything around ActorRef[-T]
• do not use macros or type calculations that Java
cannot do ...
Possible Plan
• add type parameter to ActorRef, Actor, …
• remove sender()
• type Receive = PartialFunction[T, Unit]
• res...
But why stop here?
« … and determine the behavior to be
applied to the next message.»
— Carl Hewitt, 1973
gålbma (sami) — kolme (finnish): THREE
We have one chance to rectify some things
Project Gålbma
• distill an Actor to its essence: the Behavior
• everything is a message—for real this time
• remove the d...
Behavior is King, no more Actor trait
24
object Server {
sealed trait Command
case class Get(id: Int)(val replyTo: ActorRe...
No More Closing over ActorContext
• ActorContext is passed in for every message
• processing a message returns the next be...
Everything behaves like a Message
• ActorContext remains the system interface:
• spawn, stop, watch, unwatch, setReceiveTi...
27
object Client {
sealed trait Command
case class GetRef(name: String)(val replyTo: ActorRef[GetRefReply]) extends Comman...
28
object Client {
sealed trait Command
case class GetRef(name: String)(val replyTo: ActorRef[GetRefReply]) extends Comman...
29
object Client {
sealed trait Command
case class GetRef(name: String)(val replyTo: ActorRef[GetRefReply]) extends Comman...
Under the Hood
The Implementation
• independent add-on library
• layered completely on top of untyped Actors
• currently 2kLOC main + 1.7...
The most important interface: Behavior[T]
• Behaviors:
• Full, FullTotal, Total, Partial, Static
• Decorators:
• ContextAw...
ActorSystem ≈ ActorRef
33
object Demo extends App {
implicit val t = Timeout(1.second)
val guardian = ContextAware[Client....
Testing
Behavior Rulez!
• decoupling of logic from execution mechanism
• synchronous behavioral tests of individual Actors
• mock ...
36
object `A Receptionist` {
def `must register a service`(): Unit = {
val ctx = new EffectfulActorContext("register", Pro...
What can we do with it?
Encoding Types with Members
38
class MyClass {
def myMethod(id: Int): String
def otherMethod(name: String): Unit
protected...
Encoding Types with Members
• Typed Actors provide complete modules with members
• Typed Actors can encode more flexible a...
Calling Methods
40
object MyClassDemo {
import MyClass._
val myClass: MyClass = ???
val myActor: ActorRef[Command] = ???
i...
But Actors can do more: Protocols
41
object Protocol {
case class GetSession(replyTo: ActorRef[GetSessionResult])
sealed t...
But Actors can do more: Protocols
42
What can we express?
• everything a classical module with methods can
• pass object references as inputs and outputs
• pat...
What can we NOT express?
• any dynamic behavior (e.g. internal state changes)
• session invalidation
44
Summary and Outlook
Current Status
• part of Akka 2.4-M1
• http://doc.akka.io/docs/akka/2.4-M1/scala/typed.html
• only bare Actors
• no persis...
Next Steps
• proper Java API (probably in 2.4-M2)
• Receptionist plus akka-distributed-data for Cluster
• port Actor-based...
… and in the far future:
• reap internal benefits by inverting implementation:
• remove sender field (and thus Envelope)
•...
©Typesafe 2015 – All Rights Reserved
Upcoming SlideShare
Loading in …5
×

Project Gålbma – Actors vs Types

1,889 views

Published on

The Actor Model describes precisely what it means for computation to be distributed: encapsulated and isolated behaviors process messages that are sent asynchronously between them. Akka’s implementation of this model has been widely successful, but for a long time it had the restriction that Actor interactions were not statically type-checked. With the addition of the akka-typed module we have finally found a formulation that brings Actor messaging to the same type-safety as normal method invocation—if not beyond—while being simple and intuitive. In this presentation we will look at why this addition has taken so long, how it works, and what we can express with it.

Actor-skeptics beware: this may shift your world-view!

Published in: Software
0 Comments
4 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
1,889
On SlideShare
0
From Embeds
0
Number of Embeds
53
Actions
Shares
0
Downloads
17
Comments
0
Likes
4
Embeds 0
No embeds

No notes for slide

Project Gålbma – Actors vs Types

  1. 1. Project Gålbma: Actors vs Types Dr. Roland Kuhn @rolandkuhn — Akka Tech Lead
  2. 2. Motivation
  3. 3. Motivation 3 case class Get case class Got(contents: Map[String, ActorRef]) class Server extends Actor { var map = Map.empty[String, ActorRef] def receive = { case Get => sender ! Got(map) } }
  4. 4. 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. 5. 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. 6. 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. 7. 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()) } }
  8. 8. Failed Attempts
  9. 9. 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 ... }
  10. 10. Akka 2.1: Typed Channels 10
  11. 11. Akka 2.1: Typed Channels 11
  12. 12. Akka 2.1: Typed Channels 12
  13. 13. 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
  14. 14. The Solution
  15. 15. 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)) }
  16. 16. 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)) }
  17. 17. 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)) }
  18. 18. 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
  19. 19. 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
  20. 20. But why stop here?
  21. 21. « … and determine the behavior to be applied to the next message.» — Carl Hewitt, 1973
  22. 22. gålbma (sami) — kolme (finnish): THREE We have one chance to rectify some things
  23. 23. 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
  24. 24. 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)) } }
  25. 25. 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)})" }
  26. 26. 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. 27. 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. 28. 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. 29. 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) } }
  30. 30. Under the Hood
  31. 31. The Implementation • independent add-on library • layered completely on top of untyped Actors • currently 2kLOC main + 1.7kLOC tests • fully interoperable 31
  32. 32. 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]] }
  33. 33. 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()) }
  34. 34. Testing
  35. 35. Behavior Rulez! • decoupling of logic from execution mechanism • synchronous behavioral tests of individual Actors • mock ActorContext allows inspection of effects 35
  36. 36. 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) } ... }
  37. 37. What can we do with it?
  38. 38. Encoding Types with Members 38 class MyClass { def myMethod(id: Int): String def otherMethod(name: String): Unit protected def helper(arg: Double): Unit }
  39. 39. 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] = ??? }
  40. 40. 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) }
  41. 41. 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 }
  42. 42. But Actors can do more: Protocols 42
  43. 43. 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
  44. 44. What can we NOT express? • any dynamic behavior (e.g. internal state changes) • session invalidation 44
  45. 45. Summary and Outlook
  46. 46. 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
  47. 47. 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 JoinCalculus) • listen to community feedback 47
  48. 48. … 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
  49. 49. ©Typesafe 2015 – All Rights Reserved

×