Awesome
Note: For Akka 2.4, you have to add -a24
to the version number. For example 1.1.0
becomes 1.1.0-a24
.
The first version available for Akka 2.4 is 1.4.0
.
libraryDependencies ++= List(
"de.knutwalker" %% "typed-actors" % "1.6.0",
"de.knutwalker" %% "typed-actors-creator" % "1.6.0"
)
Documentation
- Motivation
- Basic Usage
- Union typed actors
- Unsafe Usage
- TypedActor
- Building Props
- Typed Creator
- Implementation Notes
- Comparison with Akka Typed
Motivation
One critique of Akka, that comes up every now and then is the lack of type safety. Actors essentially represent a PartialFunction[Any, Unit]
which is, from a type point of view, something of the worst you can have. It tells you nothing useful; Anything can go in, it might or might not be processed and if so, anything anywhere anytime can happen. It forgoes all the benefits of a statically typed language.
There are many reasons for this though, amongst others: location transparency and context.become
. While its true that only Any
allows us to model everything that can happen, it doesn't mean that everything will always happen. Not every actor gets moved around between different nodes and changes its behavior to something completely unrelated over and over again.
So, why not tell the compiler that we know something about certain actors and have it help us? We're in a statically typed language after all. We're used to compiler support when it comes to refactoring, design and composition. Why forgo this for the sake of a feature I don't want to use.
Hence, Typed Actors
!
Akka underwent some experiments itself, for example from typed-channels and typed-actors to akka-typed.
Especially the last approach, Akka Typed
is really nice and the benefit of having an ActorRef[A]
lead to the creation of this library.
Typed Actors
has the following goals:
- add a compile-time layer to existing
ActorRef
s with minimal runtime overhead - be compatible with all of the existing Akka modules, traits, and extensions in terms of composition and behavior
and the following non-goals:
- enforce an impenetrable mantle of types, don't fight the users knowledge about the actor system, those are dynamic after all
- support Java
So, let's dive in.
Basic Usage
To use Typed Actors, import the following:
import de.knutwalker.akka.typed._
The underscore/wildcard import is important to bring some implicit classes into scope.
These classes enable the actual syntax to use typed actors.
Also, Typed Actors shadows some names from akka.actor
, so you need to make sure, that you add this import after your akka imports.
import akka.actor._
import de.knutwalker.akka.typed._
Actor Definition
Using Typed Actors is, at first, similar to regular actors. It is always a good idea to define your message protocol.
sealed trait MyMessage
case class Foo(foo: String) extends MyMessage
case class Bar(bar: String) extends MyMessage
case object SomeOtherMessage
With that, define a regular actor.
class MyActor extends Actor {
def receive = {
case Foo(foo) => println(s"received a Foo: $foo")
case Bar(bar) => println(s"received a Bar: $bar")
}
}
Actor Creation
Now, use Props
and ActorOf
. These are now the ones from de.knutwalker.akka.typed
, not from akka.actor
.
scala> implicit val system = ActorSystem("foo")
system: akka.actor.ActorSystem = akka://foo
scala> val props = Props[MyMessage, MyActor]
props: de.knutwalker.akka.typed.Props[MyMessage] = Props(Deploy(,Config(SimpleConfigObject({})),NoRouter,NoScopeGiven,,),class MyActor,List())
scala> val ref = ActorOf(props, name = "my-actor")
ref: de.knutwalker.akka.typed.package.ActorRef[props.Message] = Actor[akka://foo/user/my-actor#-260681439]
This will give you an ActorRef[MyMessage]
.
There are three possible ways to create a Props
, mirroring the constructors from akka.actor.Props
.
scala> val props = Props[MyMessage, MyActor]
props: de.knutwalker.akka.typed.Props[MyMessage] = Props(Deploy(,Config(SimpleConfigObject({})),NoRouter,NoScopeGiven,,),class MyActor,List())
scala> val props = Props[MyMessage, MyActor](new MyActor)
props: de.knutwalker.akka.typed.Props[MyMessage] = Props(Deploy(,Config(SimpleConfigObject({})),NoRouter,NoScopeGiven,,),class akka.actor.TypedCreatorFunctionConsumer,List(class MyActor, <function0>))
scala> val props = Props[MyMessage, MyActor](classOf[MyActor])
props: de.knutwalker.akka.typed.Props[MyMessage] = Props(Deploy(,Config(SimpleConfigObject({})),NoRouter,NoScopeGiven,,),class MyActor,List())
Sending messages
Sending messages to a typed actor is the same as sending messages to an untyped on, you use !
.
scala> ref ! Foo("foo")
received a Foo: foo
scala> ref ! Bar("bar")
received a Bar: bar
If you try to send a message from a different protocol, you will get a compile error. Hooray, benefit!
scala> ref ! SomeOtherMessage
<console>:31: error: type mismatch;
found : SomeOtherMessage.type
required: ref.Message
(which expands to) MyMessage
ref ! SomeOtherMessage
^
Ask pattern
Typed actors support the ask pattern, ?
, without imports and the returned Future is properly typed.
In order to achieve this, instead of sending an already instantiated type, you send a function that, given the properly typed sender, will return the message.
This is usually achieved with a separate parameter list on a case class (message), typically called replyTo
.
case class MyResponse(payload: String)
case class MyMessage(payload: String)(val replyTo: ActorRef[MyResponse])
If you define your messages this way, you can left out the last parameter list and will get the required function.
To respond, use message.replyTo
instead of sender()
to get the properly typed sender. Although, to be fair, sender()
will be the same actor, it's just the untyped version.
Finally, ?
requires an implicit Timeout
, just like the regular, untyped ask.
import scala.concurrent.duration._
import akka.util.Timeout
class MyActor extends Actor {
def receive = {
case m@MyMessage(payload) => m.replyTo ! MyResponse(payload)
}
}
implicit val timeout: Timeout = 1.second
scala> val ref = ActorOf(Props[MyMessage, MyActor])
ref: de.knutwalker.akka.typed.package.ActorRef[MyMessage] = Actor[akka://foo/user/$a#-1898850520]
scala> val future = ref ? MyMessage("foo")
future: scala.concurrent.Future[MyResponse] = scala.concurrent.impl.Promise$DefaultPromise@75cf45c8
scala> val response = scala.concurrent.Await.result(future, 1.second)
response: MyResponse = MyResponse(foo)
Next up, learn how to mix multiple unrelated messages into the checked type.
Union typed actors
Unrelated Messages
The actor messages before were defined as a sealed trait so that the actor can deal with all subclasses of this trait. That way the actor can deal with multiple types of messages.
This works great if you're in control of the messages, unfortunately this is not always the case. Sometimes you have to write an actor that receives multiple messages that are not part of the same sealed trait, possibly because you don't own said messages.
To still use Typed Actors
, you could use Any
, which is just as bad as using untyped actors directly.
Alternatively, you could use a sum type like Either
, define the actor as ActorRef[Either[A, B]]
and pattern match on the either in the receive block. This has some drawbacks though.
First, listing more than 2 different messages with Either gets very tedious and you'll probably start writing specific custom sum types for each different set of messages and end up with sealed traits that do nothing but wrap other messages and are thus just noisy boilerplate.
Second, there is a runtime overhead involved of wrapping and unwrapping the message in the sum type, i.e. you have to apply
and unapply
the Left
/Right
instances.
Third, and probably the most disruptive one, you cannot send any of the summed types directly but have to wrap them at tellsite, coupling the actor to the chosen sum type. This also means, that you cannot write proxy-like actors that sit in-between other actors because you have to change the messages.
Typed Actors
offer an easy alternative, that solves all the aforementioned problems: Union Types.
Both, ActorRef[A]
and Props[A]
, have a or[B]
method, that turns those types into an ActorRef[A | B]
or a Props[A | B]
, respectively.
A | B
is a so called union type (also sometimes called a disjoint or discriminated union) meaning it is either A
or B
. In this regard, it serves the same purpose as Either[A, B]
, but it is a pure type-level construct. There is no runtime value possible for A | B
, it is intended to be used a phantom type to allow the compiler to apply specific constraints on certain methods.
You, as a library user, needn't worry about this; just read A | B
as A or B
.
As a side note, the implementation is different than the one provided by Miles, referenced in the link above and, dare I say, simpler; it's not based on Curry-Howard isomorphism and doesn't require unicode to type.
You can call or
multiple times, creating an ever-growing union type. For example ActorRef[A].or[B].or[C].or[D]
yields ActorRef[A | B | C | D]
, which just reads A or B or C or D
. There is no restriction on the length (certainly not at 22), although compile times will suffer for very large union types.
This solves the first problem, enumerating many types just works naturally. To be fair, this is mainly due to the infix notation. You could write A Either B Either C
as well, but that's just weird while A | B | C
comes naturally.
As mentioned before, |
is a pure typelevel construct—there is no runtime value, not event a simple wrapper. This fact solves both, the aforementioned second and third issue. Since there is not even a valid runtime representation, there can be no overhead and there is no wrapping required at tellsite.
Okay, enough theory – lets see union types in action.
Union types
First, let's define some unrelated messages. Note that these are not part of a sealed trait hierarchy.
case class Foo(foo: String)
case class Bar(bar: String)
case class Baz(baz: String)
case object SomeOtherMessage
Now, let's define an actor that receives all of these messages.
class MyActor extends Actor {
def receive = {
case Foo(foo) => println(s"received a Foo: $foo")
case Bar(bar) => println(s"received a Bar: $bar")
case Baz(baz) => println(s"received a Baz: $baz")
}
}
Define a Props
for one of those messages.
scala> val props: Props[Foo] = Props[Foo, MyActor]
props: de.knutwalker.akka.typed.Props[Foo] = Props(Deploy(,Config(SimpleConfigObject({})),NoRouter,NoScopeGiven,,),class MyActor,List())
Now just list the other message types using or
, either on the Props
or on a created ActorRef
.
scala> val props2: Props[Foo | Bar] = props.or[Bar]
props2: de.knutwalker.akka.typed.Props[de.knutwalker.akka.typed.|[Foo,Bar]] = Props(Deploy(,Config(SimpleConfigObject({})),NoRouter,NoScopeGiven,,),class MyActor,List())
scala> val ref2: ActorRef[Foo | Bar] = ActorOf(props2, name = "my-actor")
ref2: de.knutwalker.akka.typed.ActorRef[de.knutwalker.akka.typed.|[Foo,Bar]] = Actor[akka://foo/user/my-actor#-1800143858]
scala> val ref: ActorRef[Foo | Bar | Baz] = ref2.or[Baz]
ref: de.knutwalker.akka.typed.ActorRef[de.knutwalker.akka.typed.|[de.knutwalker.akka.typed.|[Foo,Bar],Baz]] = Actor[akka://foo/user/my-actor#-1800143858]
Now you can send either one of the messages that are listed in the union type.
scala> ref ! Foo("foo")
received a Foo: foo
scala> ref ! Bar("bar")
received a Bar: bar
scala> ref ! Baz("baz")
received a Baz: baz
And if you try to send a message that is not part of the type union, you will get a compile error.
scala> ref ! SomeOtherMessage
<console>:32: error: Cannot prove that message of type SomeOtherMessage.type is a member of ref.Message.
ref ! SomeOtherMessage
^
As you can see, there are no wrappers involved. When you send the message, the compiler checks that the message you want to send is part of the union and if this checks succeeds, the compiler will allow the call to !
(by not failing to compile).
Since there can be no runtime value of the union type, there is a clear distinction for the dispatch to the check if the message itself is the specified type or a subtype thereof and the check if the message is part of the specified union type.
You can turn an actor that accepts an union type into of its subcases with only
:
scala> ref.only[Foo]
res4: de.knutwalker.akka.typed.package.ActorRef[Foo] = Actor[akka://foo/user/my-actor#-1800143858]
scala> ref.only[Bar]
res5: de.knutwalker.akka.typed.package.ActorRef[Bar] = Actor[akka://foo/user/my-actor#-1800143858]
scala> ref.only[Baz]
res6: de.knutwalker.akka.typed.package.ActorRef[Baz] = Actor[akka://foo/user/my-actor#-1800143858]
Which checks the untion type as well.
scala> ref.only[SomeOtherMessage]
<console>:31: error: not found: type SomeOtherMessage
ref.only[SomeOtherMessage]
^
Union types will return later; for now, the next part is to learn how to interact with the less safer parts of Akka.
Unsafe Usage
scala> val typedRef = ActorOf[MyMessage](props, name = "my-actor")
typedRef: de.knutwalker.akka.typed.ActorRef[MyMessage] = Actor[akka://foo/user/my-actor#-760225152]
Autoreceived Messages
Some messages are automatically handled by some actors and need or cannot be provided in the actors type.
One example is PoisonPill
. To sent those kind of messages anyway, use unsafeTell
.
scala> typedRef.unsafeTell(PoisonPill)
Switch Between Typed and Untyped
Also, some Akka APIs require you to pass an untyped ActorRef (the regular ActorRef).
You can easily turn your typed actor into an untyped one bu using untyped
.
scala> val untypedRef = typedRef.untyped
untypedRef: de.knutwalker.akka.typed.package.UntypedActorRef = Actor[akka://foo/user/my-actor#-760225152]
For convenience, akka.actor.ActorRef
is type aliased as de.knutwalker.akka.typed.UntypedActorRef
.
Similarly, you can turn any untyped ref into a typed one using typed
.
scala> val typedAgain = untypedRef.typed[MyMessage]
typedAgain: de.knutwalker.akka.typed.package.ActorRef[MyMessage] = Actor[akka://foo/user/my-actor#-760225152]
As scala tends to infer Nothing
as the most specific bottom type, you want to make sure to always provide a useful type.
scala> untypedRef.typed
res1: de.knutwalker.akka.typed.package.ActorRef[Nothing] = Actor[akka://foo/user/my-actor#-760225152]
Compiletime only
There are no compiler checks to make sure, that the given actually is able to receive that kind of message. This signifies the point, that Typed Actors are really just a compile-time wrapper and do not carry any kind of runtime information. To further demonstrate this, you can see that both instances are actually the very same (despite the scalac warning).
scala> typedRef eq untypedRef
<console>:29: warning: AnyRef{type Message = MyMessage; type Self = de.knutwalker.akka.typed.UntypedActorRef} and akka.actor.ActorRef are unrelated: they will most likely never compare equal
typedRef eq untypedRef
^
res2: Boolean = true
Divergence
This also means, that it is possible to diverge from the specified type with context.become
.
scala> class MyOtherActor extends Actor {
| def receive = LoggingReceive {
| case Foo(foo) => println(s"received a Foo: $foo")
| case Bar(bar) => context become LoggingReceive {
| case SomeOtherMessage => println("received some other message")
| }
| }
| }
defined class MyOtherActor
scala> val otherRef = ActorOf(Props[MyMessage, MyOtherActor], "my-other-actor")
otherRef: de.knutwalker.akka.typed.package.ActorRef[MyMessage] = Actor[akka://foo/user/my-other-actor#253923972]
scala> otherRef ! Foo("foo")
[DEBUG] received handled message Foo(foo)
received a Foo: foo
scala> otherRef ! Bar("bar")
[DEBUG] received handled message Bar(bar)
scala> otherRef ! Foo("baz")
[DEBUG] received unhandled message Foo(baz)
scala> otherRef.untyped ! SomeOtherMessage
[DEBUG] received handled message SomeOtherMessage
received some other message
Making sure, that this cannot happen is outside of the scope of Typed Actors.
There is, however, a TypedActor
trait which tries to provide some help. Learn about it next.
TypedActor
Having a typed reference to an actor is one thing, but how can we improve type-safety within the actor itself?
Typed Actors offers a trait
called TypedActor
which you can extend from instead of Actor
.
TypedActor
itself extends Actor
but contains an abstract type member and typed receive method
instead of just an untyped receive method.
In order to use the TypedActor
, you have to extend TypedActor.Of[_]
and provide your message type via type parameter.
scala> class MyActor extends TypedActor.Of[MyMessage] {
| def typedReceive = {
| case Foo(foo) => println(s"received a Foo: $foo")
| case Bar(bar) => println(s"received a Bar: $bar")
| }
| }
defined class MyActor
scala> val ref = ActorOf(Props[MyMessage, MyActor], name = "my-actor")
ref: de.knutwalker.akka.typed.package.ActorRef[MyMessage] = Actor[akka://foo/user/my-actor#-685633214]
scala> ref ! Foo("foo")
received a Foo: foo
scala> ref ! Bar("bar")
received a Bar: bar
If you match on messages from a different type, you will get a compile error.
scala> class MyActor extends TypedActor.Of[MyMessage] {
| def typedReceive = {
| case SomeOtherMessage => println("received some other message")
| }
| }
<console>:22: error: pattern type is incompatible with expected type;
found : SomeOtherMessage.type
required: MyActor.this.Message
(which expands to) MyMessage
case SomeOtherMessage => println("received some other message")
^
Divergence
Similar to the untyped actor, context.become
is not hidden and can still lead to diverging actors.
scala> class MyOtherActor extends TypedActor.Of[MyMessage] {
| def typedReceive = {
| case Foo(foo) => println(s"received a Foo: $foo")
| case Bar(bar) => context become LoggingReceive {
| case SomeOtherMessage => println("received some other message")
| }
| }
| }
defined class MyOtherActor
scala> val otherRef = ActorOf(Props[MyMessage, MyOtherActor], "my-other-actor")
otherRef: de.knutwalker.akka.typed.package.ActorRef[MyMessage] = Actor[akka://foo/user/my-other-actor#-1609403868]
scala> otherRef ! Foo("foo")
scala> otherRef ! Bar("bar")
[DEBUG] received handled message Foo(foo)
received a Foo: foo
[DEBUG] received handled message Bar(bar)
scala> otherRef ! Foo("baz")
[DEBUG] received unhandled message Foo(baz)
scala> otherRef.untyped ! SomeOtherMessage
[DEBUG] received handled message SomeOtherMessage
received some other message
More Typing
The TypedActor
offers some more methods that ought to help with keeping within the defined type bound.
There is typedSelf
, which is the typed version of the regular self
.
Then there is typedBecome
, the typed version of context.become
. It takes a partial receive function, much like typedReceive
.
Using typedBecome
, diverging from the type bound is no longer possible
scala> class MyOtherActor extends TypedActor.Of[MyMessage] {
| def typedReceive = {
| case Foo(foo) => println(s"received a Foo: $foo")
| case Bar(bar) => typedBecome {
| case SomeOtherMessage => println("received some other message")
| }
| }
| }
<console>:31: error: pattern type is incompatible with expected type;
found : SomeOtherMessage.type
required: MyOtherActor.this.Message
(which expands to) MyMessage
case SomeOtherMessage => println("received some other message")
^
You can event get exhaustiveness checks from the compiler by using the Total
wrapper.
scala> class MyOtherActor extends TypedActor.Of[MyMessage] {
| def typedReceive = Total {
| case Foo(foo) => println(s"received a Foo: $foo")
| }
| }
<console>:25: warning: match may not be exhaustive.
It would fail on the following input: Bar(_)
def typedReceive = Total {
^
defined class MyOtherActor
Please be aware of a bug feature that wouldn't fail on non-exhaustive checks.
If you use guards in your matchers, the complete pattern is optimistically treated as exhaustive; See SI-5365, SI-7631, and SI-9232. Note the missing non-exhaustiveness warning in the next example.
scala> val False = false
False: Boolean = false
scala> class MyOtherActor extends TypedActor.Of[MyMessage] {
| def typedReceive = Total {
| case Foo(foo) if False =>
| }
| }
defined class MyOtherActor
Unfortunately, this cannot be worked around by library code. Even worse, this would not result in a unhandled message but in a runtime match error.
Working with Union Types
Union typed before were declared on an already existing Props
or ActorRef
but how can we use union types together with TypedActor
?
case class Foo(foo: String)
case class Bar(bar: String)
case class Baz(baz: String)
case object SomeOtherMessage
(We're shadowing the previous definition of Foo
and Bar
here, they are reverted after this chapter).
Since union types are implemented at the type-level, there is no runtime value possible that would allow us to discriminate between those subtypes when running the receive block.
scala> class MyActor extends TypedActor.Of[Foo | Bar | Baz] {
| def typedReceive: TypedReceive = {
| case Foo(foo) => println(s"received a Foo: $foo")
| case Bar(bar) => println(s"received a Bar: $bar")
| case Baz(baz) => println(s"received a Baz: $baz")
| }
| }
<console>:29: error: constructor cannot be instantiated to expected type;
found : Foo
required: de.knutwalker.akka.typed.|[de.knutwalker.akka.typed.|[Foo,Bar],Baz]
case Foo(foo) => println(s"received a Foo: $foo")
^
<console>:30: error: constructor cannot be instantiated to expected type;
found : Bar
required: de.knutwalker.akka.typed.|[de.knutwalker.akka.typed.|[Foo,Bar],Baz]
case Bar(bar) => println(s"received a Bar: $bar")
^
<console>:31: error: constructor cannot be instantiated to expected type;
found : Baz
required: de.knutwalker.akka.typed.|[de.knutwalker.akka.typed.|[Foo,Bar],Baz]
case Baz(baz) => println(s"received a Baz: $baz")
^
We have to do this discrimination at type-level as well. Don't worry, it's less complicated as that sound. As a side note, sum types like Either
are sometimes referred to as tagged union, the tag being the thing that would help us to discrimite at runtime – our union type is an untagged union instead.
The basics stay the same, you still extends TypedActor.Of
and implement typedReceive
but this time using either Union
or TotalUnion
. Use Union
if you only cover some of the union types cases and TotalUnion
if you want to cover all cases. The compiler can perform exhaustiveness checks on the latter.
Both methods return a builder-style object that has an on
method that must be used to enumerate the individual subcases of the union type and you close with a call to apply
.
scala> class MyActor extends TypedActor.Of[Foo | Bar | Baz] {
| def typedReceive: TypedReceive = Union
| .on[Foo]{ case Foo(foo) => println(s"received a Foo: $foo") }
| .on[Bar]{ case Bar(bar) => println(s"received a Bar: $bar") }
| .on[Baz]{ case Baz(baz) => println(s"received a Baz: $baz") }
| .apply
| }
defined class MyActor
Or if you have a total function for the cases, there is a shortcut:
scala> class MyActor extends TypedActor.Of[Foo | Bar | Baz] {
| def typedReceive: TypedReceive = Union
| .total[Foo]{ foo ⇒ println(s"received a Foo: $foo.foo") }
| .total[Bar]{ bar ⇒ println(s"received a Bar: $bar.bar") }
| .total[Baz]{ baz ⇒ println(s"received a Baz: $baz.baz") }
| .apply
| }
defined class MyActor
You have to provide at least one case, you cannot define an empty behavior.
scala> class MyActor extends TypedActor.Of[Foo | Bar | Baz] {
| def typedReceive: TypedReceive = Union
| .apply
| }
<console>:29: error: Cannot prove that de.knutwalker.akka.typed.TypedActor.MkPartialUnionReceive.Empty =:= de.knutwalker.akka.typed.TypedActor.MkPartialUnionReceive.NonEmpty.
.apply
^
If you remove one of those cases it still compiles, since Union
does not check for exhaustiveness.
scala> class MyActor extends TypedActor.Of[Foo | Bar | Baz] {
| def typedReceive: TypedReceive = Union
| .on[Foo]{ case Foo(foo) => println(s"received a Foo: $foo") }
| .on[Baz]{ case Baz(baz) => println(s"received a Baz: $baz") }
| .apply
| }
defined class MyActor
If you switch to TotalUnion
you can see the compiler message telling that something is missing. Unfortunately it doesn't tell you which case is missing exactly, although that might change in the future.
scala> class MyActor extends TypedActor.Of[Foo | Bar | Baz] {
| def typedReceive: TypedReceive = TotalUnion
| .on[Foo]{ case Foo(foo) => println(s"received a Foo: $foo") }
| .on[Baz]{ case Baz(baz) => println(s"received a Baz: $baz") }
| .apply
| }
<console>:31: error: Cannot prove that de.knutwalker.akka.typed.|[Foo,Baz] contains the same members as de.knutwalker.akka.typed.|[de.knutwalker.akka.typed.|[Foo,Bar],Baz].
.apply
^
You can even leave out the call to apply
.
scala> class MyActor extends TypedActor.Of[Foo | Bar | Baz] {
| def typedReceive: TypedReceive = Union
| .on[Foo]{ case Foo(foo) ⇒ println(s"received a Foo: $foo") }
| .on[Baz]{ case Baz(baz) ⇒ println(s"received a Baz: $baz") }
| }
defined class MyActor
Which is true for TotalUnion
as well.
scala> class MyActor extends TypedActor.Of[Foo | Bar | Baz] {
| def typedReceive: TypedReceive = TotalUnion
| .on[Foo]{ case Foo(foo) ⇒ println(s"received a Foo: $foo") }
| .on[Bar]{ case Bar(bar) ⇒ println(s"received a Bar: $bar") }
| .on[Baz]{ case Baz(baz) ⇒ println(s"received a Baz: $baz") }
| }
defined class MyActor
As you can see, you basically provide a receive block for all relevant subtypes of the union. One such receive block is typed in its input, though you cannot use the Total
helper as this one is fixed on the complete message type, the union type itself in this case.
scala> class MyActor extends TypedActor.Of[Foo | Bar | Baz] {
| def typedReceive: TypedReceive = Union
| .on[Foo](Total { case Foo(foo) => println(s"received a Foo: $foo") })
| .apply
| }
<console>:29: error: constructor cannot be instantiated to expected type;
found : Foo
required: de.knutwalker.akka.typed.|[de.knutwalker.akka.typed.|[Foo,Bar],Baz]
.on[Foo](Total { case Foo(foo) => println(s"received a Foo: $foo") })
^
At any rate, the Props
and ActorRef
from this TypedActor
are union typed as well.
scala> val props = PropsFor[MyActor]
props: de.knutwalker.akka.typed.Props[MyActor#Message] = Props(Deploy(,Config(SimpleConfigObject({})),NoRouter,NoScopeGiven,,),class MyActor,List())
scala> val ref = ActorOf(props)
ref: de.knutwalker.akka.typed.package.ActorRef[props.Message] = Actor[akka://foo/user/$a#222597801]
scala> ref ! Foo("foo")
[DEBUG] received handled message Foo(foo)
received a Foo: foo
scala> ref ! Bar("bar")
[DEBUG] received handled message Bar(bar)
received a Bar: bar
scala> ref ! Baz("baz")
[DEBUG] received handled message Baz(baz)
received a Baz: baz
scala> ref ! SomeOtherMessage
<console>:32: error: Cannot prove that message of type SomeOtherMessage.type is a member of ref.Message.
ref ! SomeOtherMessage
^
If you want to context.become
with a union type there are some options.
- You can use the
Union
/TotalUnion
helper as described earlier. - You can use
unionBecome
if you only want to cover one particular case. It is a shortcut fortypedBecome(Union.on[Msg]{ case ... }.apply)
scala> class MyActor extends TypedActor.Of[Foo | Bar | Baz] {
| def typedReceive: TypedReceive = Union
| .on[Foo]{
| case Foo(foo) =>
| unionBecome.on[Bar] {
| case Bar(bar) => println(s"received a Boo: $bar")
| }
| }
| .apply
| }
defined class MyActor
Stateless actor from a total function
The companion object TypedActor
has an apply
method that wraps a total function in an actor and returns a prop for this actor.
scala> val ref = ActorOf(TypedActor[MyMessage] {
| case Foo(foo) => println(s"received a Foo: $foo")
| case Bar(bar) => println(s"received a Bar: $bar")
| })
ref: de.knutwalker.akka.typed.package.ActorRef[MyMessage] = Actor[akka://foo/user/$b#509742201]
Low-level TypedActor
You can also directly extend TypedActor
, in which case you have to implement the abstract type Message
. The Of
constructor just does this for you by getting all information from the defined type parameter.
You want to use this you need the TypedActor
as a trait, for example when mixing it together with other Actor traits, like PersistenActor
.
For normal use-case, extending TypedActor.Of[_]
is encouraged.
scala> import scala.reflect.classTag
import scala.reflect.classTag
scala> class MyTypedActor extends TypedActor {
| type Message = MyMessage
|
| def typedReceive = {
| case Foo(foo) =>
| }
| }
defined class MyTypedActor
You can even override the receive
method, if you have to, using the untypedFromTyped
method.
scala> class MyTypedActor extends TypedActor {
| type Message = MyMessage
|
| override def receive =
| untypedFromTyped(typedReceive)
|
| def typedReceive = {
| case Foo(foo) =>
| }
| }
defined class MyTypedActor
Using this, you can mix a TypedActor
and a PersistentActor
together.
scala> import akka.persistence.PersistentActor
import akka.persistence.PersistentActor
scala> class TypedPersistentActor extends TypedActor with PersistentActor with ActorLogging {
| type Message = MyMessage
|
| def persistenceId: String = "typed-persistent-id"
|
| val receiveRecover: Receive = akka.actor.Actor.emptyBehavior
|
| val typedReceive: TypedReceive = {
| case foo: Foo =>
| persist(foo)(f => context.system.eventStream.publish(foo))
| }
|
| val receiveCommand: Receive =
| untypedFromTyped(typedReceive)
|
| override def receive: Receive =
| receiveCommand
| }
defined class TypedPersistentActor
Going back to untyped land
Sometimes you have to receive messages that are outside of your protocol. A typical case is Terminated
, but other modules and patterns have those messages as well.
You can use Untyped
to specify a regular untyped receive block, just as if receive
were actually the way to go. Untyped
also works with union types without any special syntax.
scala> class MyOtherActor extends TypedActor.Of[MyMessage] {
| def typedReceive = Untyped {
| case Terminated(ref) => println(s"$ref terminated")
| case Foo(foo) => println(s"received a Foo: $foo")
| }
| }
defined class MyOtherActor
With Untyped
, you won't get any compiler support, it is meant as an escape hatch; If you find yourself using Untyped
all over the place, consider just using a regular Actor
instead.
Next, learn more ways to create Props
.
Building Props
Message Type Derivation
When creating a props for a TypeActor
, we can derive the message type and thus reduce the amount of type annotation we have to write.
This is done with PropsFor
.
Consider this typed actor.
class MyActor extends TypedActor.Of[MyMessage] {
def typedReceive = {
case Foo(foo) => println(s"received a Foo: $foo")
}
}
Using Props
we have to repeat the information, that this actor only accepts messages of type MyMessage
, although the compiler knows about this.
scala> Props[MyMessage, MyActor] // MyMessage is repetitive
res0: de.knutwalker.akka.typed.Props[MyMessage] = Props(Deploy(,Config(SimpleConfigObject({})),NoRouter,NoScopeGiven,,),class MyActor,List())
scala> Props(new MyActor) // message type derives as Nothing
res1: de.knutwalker.akka.typed.package.Props[Nothing] = Props(Deploy(,Config(SimpleConfigObject({})),NoRouter,NoScopeGiven,,),class akka.actor.TypedCreatorFunctionConsumer,List(class MyActor, <function0>))
scala> Props[MyMessage, MyActor](new MyActor) // MyMessage and MyActor are repetitive
res2: de.knutwalker.akka.typed.Props[MyMessage] = Props(Deploy(,Config(SimpleConfigObject({})),NoRouter,NoScopeGiven,,),class akka.actor.TypedCreatorFunctionConsumer,List(class MyActor, <function0>))
scala> Props(classOf[MyActor]) // message type derives as Nothing
res3: Object{type Message = Nothing; type Self = akka.actor.Props} = Props(Deploy(,Config(SimpleConfigObject({})),NoRouter,NoScopeGiven,,),class MyActor,List())
scala> Props[MyMessage, MyActor](classOf[MyActor]) // MyMessage and MyActor are repetitive
res4: de.knutwalker.akka.typed.Props[MyMessage] = Props(Deploy(,Config(SimpleConfigObject({})),NoRouter,NoScopeGiven,,),class MyActor,List())
When you have a TypedActor
, you can use PropsFor
instead of Props
to use the type information embedded in TypedActor#Message
.
scala> PropsFor[MyActor]
res5: de.knutwalker.akka.typed.Props[MyActor#Message] = Props(Deploy(,Config(SimpleConfigObject({})),NoRouter,NoScopeGiven,,),class MyActor,List())
scala> PropsFor(new MyActor)
res6: de.knutwalker.akka.typed.package.Props[MyActor#Message] = Props(Deploy(,Config(SimpleConfigObject({})),NoRouter,NoScopeGiven,,),class akka.actor.TypedCreatorFunctionConsumer,List(class MyActor, <function0>))
scala> PropsFor(classOf[MyActor])
res7: Object{type Message = MyMessage; type Self = akka.actor.Props} = Props(Deploy(,Config(SimpleConfigObject({})),NoRouter,NoScopeGiven,,),class MyActor,List())
Of course, some of these cases can also be mitigated by using type ascription on the result type.
scala> val props: Props[MyMessage] = Props(new MyActor)
props: de.knutwalker.akka.typed.Props[MyMessage] = Props(Deploy(,Config(SimpleConfigObject({})),NoRouter,NoScopeGiven,,),class akka.actor.TypedCreatorFunctionConsumer,List(class MyActor, <function0>))
scala> val props: Props[MyMessage] = Props(classOf[MyActor])
props: de.knutwalker.akka.typed.Props[MyMessage] = Props(Deploy(,Config(SimpleConfigObject({})),NoRouter,NoScopeGiven,,),class MyActor,List())
scala> val props: Props[MyMessage] = PropsFor[MyActor]
props: de.knutwalker.akka.typed.Props[MyMessage] = Props(Deploy(,Config(SimpleConfigObject({})),NoRouter,NoScopeGiven,,),class MyActor,List())
scala> val props: Props[MyMessage] = PropsFor(new MyActor)
props: de.knutwalker.akka.typed.Props[MyMessage] = Props(Deploy(,Config(SimpleConfigObject({})),NoRouter,NoScopeGiven,,),class akka.actor.TypedCreatorFunctionConsumer,List(class MyActor, <function0>))
scala> val props: Props[MyMessage] = PropsFor(classOf[MyActor])
props: de.knutwalker.akka.typed.Props[MyMessage] = Props(Deploy(,Config(SimpleConfigObject({})),NoRouter,NoScopeGiven,,),class MyActor,List())
Type Currying for Props
PropsFor
only works with a TypedActor
. There is yet another way to create a Props
, that has the type information curried, PropsOf
.
With PropsOf
, you apply once with the message type and then use one of the three ways to create a Props
. This works for all actors
scala> PropsOf[MyMessage][MyActor]
res8: de.knutwalker.akka.typed.Props[MyMessage] = Props(Deploy(,Config(SimpleConfigObject({})),NoRouter,NoScopeGiven,,),class MyActor,List())
scala> PropsOf[MyMessage](new MyActor)
res9: de.knutwalker.akka.typed.Props[MyMessage] = Props(Deploy(,Config(SimpleConfigObject({})),NoRouter,NoScopeGiven,,),class akka.actor.TypedCreatorFunctionConsumer,List(class MyActor, <function0>))
scala> PropsOf[MyMessage](classOf[MyActor])
res10: de.knutwalker.akka.typed.Props[MyMessage] = Props(Deploy(,Config(SimpleConfigObject({})),NoRouter,NoScopeGiven,,),class MyActor,List())
Next, look at how you can improve type safety even further.
Typed Creator
When creating a Props
, the preferred way is to use the (Class[_], Any*)
overload, since this one does not create a closure.
If you create a props from within an Actor using the (=> Actor)
overload, you accidentally close over the ActorContext
, that's shared state you don't want.
The problem with the constructor using Class
, you don't get any help from the compiler. If you change one parameter, there is nothing telling you to change the Props constructor but the eventual runtime error (from your tests, hopefully).
Using shapeless, we can try to fix this issue.
Using the creator module
The types creator lives in a separate module that you have to include first.
libraryDependencies += "de.knutwalker" %% "typed-actors-creator" % "1.6.0"
Next, you have to use the TypedActor
trait and you have to make your actor a case class
.
This is necessary, so that shapeless' generic machinery can pick up the required constructor parameters.
case class MyActor(param: String) extends TypedActor.Of[MyMessage] {
def typedReceive = {
case Foo(foo) => println(s"$param - received a Foo: $foo")
case Bar(bar) => println(s"$param - received a Bar: $bar")
}
}
Next, use the Typed
constructor. It takes one type parameter, which is supposed to be your TypedActor
.
Now you can use two methods, props
and create
. Both accept the same arguments as the constructor of your TypedActor
and will either return a typed Props
or typed ActorRef
, respectively (thanks to some shapeless magic).
scala> Typed[MyActor].props("Bernd")
res0: de.knutwalker.akka.typed.Props[MyMessage] = Props(Deploy(,Config(SimpleConfigObject({})),NoRouter,NoScopeGiven,,),class akka.actor.TypedCreatorFunctionConsumer,List(class MyActor, <function0>))
scala> Typed[MyActor].create("Bernd")
res1: de.knutwalker.akka.typed.ActorRef[MyMessage] = Actor[akka://foo/user/$a#-228053190]
scala> ActorOf(Typed[MyActor].props("Bernd"), "typed-bernd")
res2: de.knutwalker.akka.typed.package.ActorRef[MyMessage] = Actor[akka://foo/user/typed-bernd#1603161306]
Wrong invocations are greeted with a compile error instead of a runtime error!
scala> Typed[MyActor].create()
<console>:26: error: type mismatch;
found : shapeless.HNil
required: shapeless.::[String,shapeless.HNil]
Typed[MyActor].create()
^
scala> Typed[MyActor].create("Bernd", "Ralf")
<console>:26: error: type mismatch;
found : shapeless.::[String("Bernd"),shapeless.::[String("Ralf"),shapeless.HNil]]
required: shapeless.::[String,shapeless.HNil]
Typed[MyActor].create("Bernd", "Ralf")
^
scala> Typed[MyActor].create(42)
<console>:26: error: type mismatch;
found : shapeless.::[Int(42),shapeless.HNil]
required: shapeless.::[String,shapeless.HNil]
Typed[MyActor].create(42)
^
Hooray, Benefit!
As you can see, shapeless leaks in the error messages, but you can still easily see what parameters are wrong.
This technique uses whitebox macros under the hood, which means that support from IDEs such as IntelliJ will be meager, so prepare for red, squiggly lines.
If you open autocomplete on a Typed[MyActor]
, you won't see the create
or props
methods but createProduct
and propsProduct
. This is a leaky implementation as well, better just ignore it and type against those IDE errors.
The next bits are about the internals and some good pratices..
Implementation Notes
Typed Actors are implemented as a type tag, a structural type refinement.
This is very similar to scalaz.@@
and a little bit to shapeless.tag.@@
The message type is put together with the surrounding type (ActorRef
or Props
) into a special type, that exists only at compile time.
It carries enough type information for the compiler reject certain calls to tell while not requiring any wrappers at runtime.
The actual methods are provided by an implicit ops wrapper that extends AnyVal, so that there is no runtime overhead as well.
The union type is inspired by shapeless' HNil
or Coproduct
. The main differences are: 1) There is no runtime, value-level representation and as such, there is no Inr/Inl/:: constructor, it's just the type |
(instead of ::
or :+:
for HList and Coproduct, respectively). 2) It doesn't have an end type, a base case like HNil
or CNil
. Other than that, the operations around the union type are similar to what you would write if you'd define a function for an HList: There is a typeclass representing the function and some implicit induction steps that recurse on the type.
There are some other union type implementations out there, including the one that is offered by shapeless itself but they often just focus on offering membership testing as functionality, while Typed Actors
also includes a union set comparison to check whether two union types cover the same elements without them being defined in the same order.
Good Practices
Typed Actors does not try to prevent you from doing fancy things and shooting yourself in the foot, it rather wants to give you a way so you can help yourself in keeping your sanity.
That is, you can always switch between untyped and typed actors, even if the type information is not actually corresponding to the actors implementation. It is up to you to decide how much safety you want to trade in for flexibility.
That being said, you get the most benefit by using the TypedActor with the Typed Creator and only on the typedReceive
and typedBecome
methods with the Total
wrapper. Depending on the situation, you can fairly fine-tune the amount of untypedness you want to have.
One other thing that is frequently causing trouble is sender()
.
For one, it's not referentially transparent, return the sender of whatever message the Actor is currently processing. This is causing trouble when the sender()
call happens for example in a callback attached to a Future
.
The other thing is, it's always an untyped actor and knowledge about the protocol has to be implicitly kept in the head of the developer.
For that reasons, it is a good idea to always provide a replyTo: ActorRef[A]
field in the message itself and refrain from using sender()
, ideally ever.
An example of how this could look like. First, the counter example using sender()
as a quasi status quo.
To have a sensible sender()
available, we're gonna use akka.actor.Inbox
.
import akka.actor.ActorDSL._
val box = inbox()
This is a typical request reply cycle using sender()
.
case class MyMessage(payload: Int)
case class MyResponse(payload: String)
case class MyActor() extends TypedActor.Of[MyMessage] {
def typedReceive = {
case MyMessage(payload) => sender() ! payload.toString
}
}
scala> val ref = Typed[MyActor].create()
ref: de.knutwalker.akka.typed.ActorRef[MyMessage] = Actor[akka://foo/user/$a#466697111]
scala> box.send(ref.untyped, MyMessage(42))
Note that there already is a bug, as the return message was not wrapped in MyResponse
.
scala> val MyResponse(response) = box.receive()
scala.MatchError: 42 (of class java.lang.String)
... 394 elided
Here's how that looks using the replyTo
pattern.
case class MyResponse(payload: String)
case class MyMessage(payload: Int)(val replyTo: ActorRef[MyResponse])
case class MyActor() extends TypedActor.Of[MyMessage] {
def typedReceive = {
case m@MyMessage(payload) => m.replyTo ! MyResponse(payload.toString)
}
}
scala> val ref = Typed[MyActor].create()
ref: de.knutwalker.akka.typed.ActorRef[MyMessage] = Actor[akka://foo/user/$b#1300424475]
scala> ref ! MyMessage(42)(box.receiver.typed)
scala> val MyResponse(response) = box.receive()
response: String = 42
Let's try to reproduce the bug from earlier.
scala> case class MyActor() extends TypedActor.Of[MyMessage] {
| def typedReceive = {
| case m@MyMessage(payload) => m.replyTo ! payload.toString
| }
| }
<console>:27: error: type mismatch;
found : String
required: m.replyTo.Message
(which expands to) MyResponse
case m@MyMessage(payload) => m.replyTo ! payload.toString
^
Now the compiler has caught the bug, benefit!
The replyTo
pattern is also important in Akka Typed.
Comparison with Akka Typed
The Akka Typed
project is a module of Akka (as of 2.4) which aims to provide typesafe actors as well.
Akka typed takes a completely different approach, mirroring most of the untyped API and ultimately offering a completely new API to define your actors behavior. Currently, this implementation sits on top of untyped They are currently actors.
Let me add that I really like Akka Typed and having worked with it for some time lead me to think about how to bring type safety to the rest of Akka.
Akka Typed
is not only about a typed ActorRef[A]
, there's much more that's changed and is reason to use Akka Typed
, both in general and over Typed Actors
. It separates the behavior of your actors from its execution model, making them really easy to test; You can just use a synchronous stub execution model and you get to test just the behavior without concerning yourself about the how-to-test-this-async-thingy. The new behavior API is not just a convoluted PartialFunction[A, Unit]
but allows you to split your behavior into nice little pieces and have them composed together. Akka Typed
's getting rid of some old (and bad) habits as well; sender()
is gone, as are lifecycle methods that have to be overridden, even the Actor
trait itself is gone. It's messages and behavior all the way down!
Those are all concerns that Typed Actor
will never deal with, this is one important difference: Typed Actors
is a possibility to add some compile-time checking while Akka Typed
is a completely new API. Understandingly, Akka Typed
is better at hiding their untyped implementation, nothing in the public API leads to the fact that something like an untyped actor could even exist.
On the other hand, having Akka Typed
as a separate module means it is difficult to use the typed API with other modules. Most APIs expect an akka.actor.ActorRef
and you can't get one from a akka-typed actor (well, you can, but it's dirty). This also applies to things like ActorLogging
and Stash
.
Typed Actors
doesn't try to prevent you from going untyped and as there is no different runtime representation, it can be easily used with all existing akka modules.
However, if you mix typed/untyped code too much, you run into unhandled messages or maybe even runtime class cast exceptions or match errors (which ought to be bugs then, really).
Typed Actors
makes it easy to deal with multiple types of messages, not just one A
thanks to its Union type support. Joining multiple behavior requires them to be of the same type, although you can get far with a little bit of type-fu. Basically, you can take advantage of the covariant nature of ActorRef[-A]
(in Typed Actors
, ActorRef is actually invariant) and create phantom intersection types (A with B
) and upcast at tellsite. It is, however, something different whether you as the library user has to know how to fu or I as the library author know so you don't have to.
Also, Akka Typed
is concerned with Java interop, which Typed Actors
is not.
Nevertheless, Akka Typed
is a – in my opinion – really nice project and its new API is a major improvement over the default Actor
. The resulting patterns, like replyTo
are a good idea to use with Typed Actor
s as well.
That concludes the Usage Guide. I guess the only thing left is to go on hAkking!
<!--- TUT:END -->Disclaimer
Typed Actor is operating on a best-effort basis to catch unsafe usages of actors at compile time, as such, it may fail to succeed in this effort. It is no replacement for tests and some interoperability issue when combining multiple scala features can lead to code that should not compile, but passes anyway; Therefore the author disclaims all warranty or liability of any kind.
Know unpleasantries
Due to PartialFunction
being contravariant in its input, Receive <: TypedReceive
is true.
If you have a receive block that is statically known to be a Receive
it can be passed without any warning to typedReceive
or other methods that actually accept a TypedReceive
.
This is a failed compiler check, there is no runtime error (if it is, it's a bug).
Scala sometime fails to perform exhaustiveness checks for total functions an various grounds. For example, val f: String => Unit = { case "foo" => () }
compiles just fine.
The exhaustiveness guarantees of various total
combinators rely soley on scalac for their checks. Therefore, if scalac doesn't warn, Typed Actors also doesn't warn.
License
This code is open source software licensed under the Apache 2.0 License.