Skip to content

Commit 1cef7c3

Browse files
committed
Provide Untyped escape for typed actors
1 parent 3424382 commit 1cef7c3

File tree

4 files changed

+184
-43
lines changed

4 files changed

+184
-43
lines changed

core/src/main/scala/de/knutwalker/akka/typed/TypedActor.scala

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,19 @@ package de.knutwalker.akka.typed
1818

1919
import _root_.akka.actor.Actor
2020
import _root_.akka.event.LoggingReceive
21-
import de.knutwalker.akka.typed.TypedActor.TypedReceiver
21+
import akka.actor.Actor.Receive
22+
import de.knutwalker.akka.typed.TypedActor.{ Downcast, TypedReceiver }
23+
24+
import scala.reflect.ClassTag
2225

2326
/**
2427
* TypedActor base trait that should be extended by to create a typed Actor.
2528
* This actor is designed to only receive one type of message in its lifetime.
2629
* Typically, this is some ADT/sealed trait that defines the protocol for this actor.
2730
*
28-
* The message type is defined by the abstract type member `Message`.
29-
* The object [[TypedActor$]] defines a helper trait to provide the message type
30-
* via a type paramter instead of a type member. These two are equivalent:
31+
* The message type is defined by extending [[TypedActor.Of]]:
3132
*
3233
* {{{
33-
* class ExampleActor extends TypeActor {
34-
* type Message = ExampleProtocol
35-
* // ...
36-
* }
37-
*
3834
* class ExampleActor extends TypeActor.Of[ExampleProtocol] {
3935
* // ...
4036
* }
@@ -59,11 +55,14 @@ import de.knutwalker.akka.typed.TypedActor.TypedReceiver
5955
* }
6056
* }}}
6157
*
58+
* If you must go back to untyped land, use the [[TypedActor#Untyped]] wrapper.
59+
*
6260
* @see [[akka.actor.Actor]] for more about Actors in general.
6361
*/
64-
trait TypedActor extends Actor {
62+
sealed trait TypedActor extends Actor {
6563
type Message
6664
type TypedReceive = PartialFunction[Message, Unit]
65+
implicit def _ct: ClassTag[Message]
6766

6867
/** Typed variant of [[self]]. */
6968
final val typedSelf: ActorRef[Message] =
@@ -79,16 +78,30 @@ trait TypedActor extends Actor {
7978
*
8079
* {{{
8180
* // error: match may not be exhaustive. It would fail on the following inputs: None
82-
* class ExampleActor extends TypedActor {
83-
* type Message = Option[String]
81+
* class ExampleActor extends TypedActor.Of[Option[String]] {
8482
* def typedReceive: TypedReceive = Total {
8583
* case Some("foo") ⇒
8684
* }
8785
* }
8886
* }}}
8987
*/
9088
final def Total(f: Message Unit): TypedReceive =
91-
PartialFunction(f)
89+
new Downcast[Message](_ct.runtimeClass.asInstanceOf[Class[Message]])(f)
90+
91+
/**
92+
* Wraps an untyped receiver and returns it as a [[TypedReceive]].
93+
* Use this to match for messages that are outside of your protocol, e.g. [[akka.actor.Terminated]].
94+
*
95+
* {{{
96+
* class ExampleActor extends TypedActor.Of[ExampleMessage] {
97+
* def typedReceive: TypedReceive = Untyped {
98+
* case Terminated(ref) => println(s"$$ref terminated")
99+
* }
100+
* }
101+
* }}}
102+
*/
103+
final def Untyped(f: Receive): TypedReceive =
104+
f // .asInstanceOf[TypedReceive]
92105

93106
/**
94107
* `TypedActor`s delegate to [[typedReceive]].
@@ -116,31 +129,38 @@ trait TypedActor extends Actor {
116129
}
117130
object TypedActor {
118131
/**
119-
* A convenience trait to provide the message type via type parameters.
120-
*
121-
* * {{{
122-
* class ExampleActor extends TypeActor {
123-
* type Message = ExampleProtocol
124-
* // ...
125-
* }
132+
* Abstract class to extend from in order to get a [[TypedActor]].
133+
* If you want to have the message type provided as a type parameter,
134+
* you have to add a context bound for [[scala.reflect.ClassTag]].
126135
*
136+
* {{{
127137
* class ExampleActor extends TypeActor.Of[ExampleProtocol] {
128138
* // ...
129139
* }
130140
* }}}
131141
*
132142
* @tparam A the message type this actor is receiving
133143
*/
134-
trait Of[A] extends TypedActor {final type Message = A}
144+
abstract class Of[A](implicit val _ct: ClassTag[A]) extends TypedActor {
145+
final type Message = A
146+
}
147+
148+
private class Downcast[A](cls: Class[A])(f: A Unit) extends Receive {
149+
def isDefinedAt(x: Any): Boolean = cls.isInstance(x)
150+
def apply(v1: Any): Unit = f(cls.cast(v1))
151+
}
152+
153+
private class TypedReceiver[A](f: PartialFunction[A, Unit]) extends Receive {
154+
private[this] val receive: Receive =
155+
f.asInstanceOf[Receive]
135156

136-
private class TypedReceiver[A](f: PartialFunction[A, Unit]) extends PartialFunction[Any, Unit] {
137157
def isDefinedAt(x: Any): Boolean = try {
138-
f.isDefinedAt(x.asInstanceOf[A])
158+
receive.isDefinedAt(x)
139159
} catch {
140160
case _: ClassCastException false
141161
}
142162

143163
def apply(x: Any): Unit =
144-
f(x.asInstanceOf[A])
164+
receive(x)
145165
}
146166
}

docs/src/tut/typed-actor.md

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ akka.actor.debug.receive=off
2424
Having a typed reference to an actor is one thing, but how can we improve type-safety within the actor itself?
2525
**Typed Actors** offers a `trait` called `TypedActor` which you can extend from instead of `Actor`.
2626
`TypedActor` itself extends `Actor` but contains an abstract type member and typed receive method
27-
instead of just an untyped receive method. In order to use the `TypedActor`, you have to provide both.
27+
instead of just an untyped receive method.
28+
In order to use the `TypedActor`, you have to extend `TypedActor.Of[_]` and provide your message type via type parameter (you cannot extend directly from `TypedActor`).
2829

2930
```tut
30-
class MyActor extends TypedActor {
31-
type Message = MyMessage
31+
class MyActor extends TypedActor.Of[MyMessage] {
3232
def typedReceive = {
3333
case Foo(foo) => println(s"received a Foo: $foo")
3434
case Bar(bar) => println(s"received a Bar: $bar")
@@ -50,20 +50,6 @@ class MyActor extends TypedActor {
5050
}
5151
```
5252

53-
#### Message as type parameter
54-
55-
The message type can also be provided as a type parameter on `TypedActor.Of[_]`.
56-
57-
```tut
58-
class MyActor extends TypedActor.Of[MyMessage] {
59-
def typedReceive = {
60-
case Foo(foo) => println(s"received a Foo: $foo")
61-
}
62-
}
63-
val ref = ActorOf(Props[MyMessage, MyActor], name = "my-actor-2")
64-
ref ! Foo("foo")
65-
```
66-
6753

6854
#### Divergence
6955

@@ -81,8 +67,7 @@ akka.actor.debug.receive=on
8167
```
8268

8369
```tut
84-
class MyOtherActor extends TypedActor {
85-
type Message = MyMessage
70+
class MyOtherActor extends TypedActor.Of[MyMessage] {
8671
def typedReceive = {
8772
case Foo(foo) => println(s"received a Foo: $foo")
8873
case Bar(bar) => context become LoggingReceive {
@@ -127,6 +112,23 @@ class MyOtherActor extends TypedActor.Of[MyMessage] {
127112
}
128113
```
129114

115+
#### Going back to untyped land
116+
117+
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.
118+
You can use `Untyped` to specify a regular untyped receive block, just as if `receive` were actually the way to go.
119+
120+
121+
```tut
122+
class MyOtherActor extends TypedActor.Of[MyMessage] {
123+
def typedReceive = Untyped {
124+
case Terminated(ref) => println(s"$ref terminated")
125+
case Foo(foo) => println(s"received a Foo: $foo")
126+
}
127+
}
128+
```
129+
130+
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.
131+
130132
Next, learn more ways to create `Props`.
131133

132134
##### [» Building Props](props.html)

project/plugins.sbt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
libraryDependencies += "org.slf4j" % "slf4j-nop" % "1.7.10"
12
resolvers += Resolver.url(
23
"tut-plugin",
34
url("http://dl.bintray.com/content/tpolecat/sbt-plugin-releases"))(
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
* Copyright 2015 Paul Horn
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package de.knutwalker.akka.typed
18+
19+
import akka.actor.{ UnhandledMessage, DeadLetter, Inbox, ActorSystem }
20+
import org.specs2.mutable.Specification
21+
import org.specs2.specification.AfterAll
22+
23+
import scala.concurrent.duration.Duration
24+
25+
import java.util.concurrent.TimeUnit
26+
27+
object TypedActorSpec extends Specification with AfterAll {
28+
sequential
29+
30+
sealed trait MyFoo
31+
case object Foo extends MyFoo
32+
case object Bar extends MyFoo
33+
case object Qux
34+
35+
implicit val system = ActorSystem("foo")
36+
val inbox = Inbox.create(system)
37+
system.eventStream.subscribe(inbox.getRef(), classOf[UnhandledMessage])
38+
39+
"The TypedActor" should {
40+
"have typed partial receive" >> {
41+
class MyActor extends TypedActor.Of[MyFoo] {
42+
def typedReceive = {
43+
case Foo => inbox.getRef() ! "received a foo"
44+
}
45+
}
46+
val ref = ActorOf(PropsFor(new MyActor))
47+
48+
ref ! Foo
49+
expectMsg("received a foo")
50+
51+
ref ! Bar
52+
expectUnhandled(Bar, ref)
53+
54+
ref.untyped ! Qux
55+
expectUnhandled(Qux, ref)
56+
57+
ref.untyped ! Nil
58+
expectUnhandled(Nil, ref)
59+
}
60+
61+
"have typed total receive" >> {
62+
class MyActor extends TypedActor.Of[MyFoo] {
63+
def typedReceive = Total {
64+
case Foo => inbox.getRef() ! "received a foo"
65+
case Bar => inbox.getRef() ! "received a bar"
66+
}
67+
}
68+
val ref = ActorOf(PropsFor(new MyActor))
69+
70+
ref ! Foo
71+
expectMsg("received a foo")
72+
73+
ref ! Bar
74+
expectMsg("received a bar")
75+
76+
ref.untyped ! Qux
77+
expectUnhandled(Qux, ref)
78+
79+
ref.untyped ! Nil
80+
expectUnhandled(Nil, ref)
81+
}
82+
83+
"have untyped partial receive" >> {
84+
class MyActor extends TypedActor.Of[MyFoo] {
85+
def typedReceive = Untyped {
86+
case Foo => inbox.getRef() ! "received a foo"
87+
case Bar => inbox.getRef() ! "received a bar"
88+
case Qux => inbox.getRef() ! "receive a qux"
89+
}
90+
}
91+
val ref = ActorOf(PropsFor(new MyActor))
92+
93+
ref ! Foo
94+
expectMsg("received a foo")
95+
96+
ref ! Bar
97+
expectMsg("received a bar")
98+
99+
ref.untyped ! Qux
100+
expectMsg("receive a qux")
101+
102+
ref.untyped ! Nil
103+
expectUnhandled(Nil, ref)
104+
}
105+
}
106+
107+
108+
109+
def expectUnhandled(message: Any, ref: ActorRef[_]) =
110+
inbox.receive(Duration(10, TimeUnit.MILLISECONDS)) === UnhandledMessage(message, system.deadLetters, ref.untyped)
111+
112+
def expectMsg(expected: Any) =
113+
inbox.receive(Duration(10, TimeUnit.MILLISECONDS)) === expected
114+
115+
116+
117+
def afterAll(): Unit = system.shutdown()
118+
}

0 commit comments

Comments
 (0)