This was intended to be a single comprehensive post about what’s wrong in mixing the Actor model with OOP. After a while I was writing this I discovered that there is a lot of stuff to be told, so I split the post in two. This is the first and talks about why you would like to add typing to Actors and then why you would like to get back. The next one (that I would likely publish in 2020) is about why you would like to add inheritance among actors and why guess what… you would refrain to do it. Let’s start.
Once that the concept of Actor as implemented by the Akka framework is clear, we can proceed to the first issue in mixing Actors and OOP, that is brilliantly depicted by the sentence “No good deed goes unpunished”.
In fact, there was a perceived problem and the solution turned out, when it was too late, worse than the problem.
As shown in my previous post, an Akka Actor has an untyped interface – messages delivered to Actor’s door may be of Any type and it is up to the pattern matcher to sort the communication out at run-time.
This has two main inconveniences. First, errors are detected (if they are) at run-time. Second, IDEs cannot help since all actors are of the same type (ActorRef) and therefore you can’t have a list of specialized actions for the actor instance under the cursor. (I haven’t written this, but the framework hides all the specific actor types behind a generic ActorRef interface. In fact, you send messages to the ActorRef interface and not directly to the derived class).
How can we get around? The idea was to provide a typed wrapper that hides the actual actor, providing a method based interface.
Let’s say that we have actor
A that accepts message
M. This will be equipped with a companion class
AA exposing a method
m() : Unit. Method
m() just builds an object of type
M and sends it to the ActorRef behind the curtains.
By employing the type of the
AA wrapper, now you (and the compiler) can check at compile-time that only proper methods can be invoked. On the other hand you are creating an impedance mismatch within your codebase, since some parts require an
ActorRef, other parts require your
AA-and-offsprings types. This leads you to open the implementation details and reveal the underlying ActorRef and, as every OOP programmer knows, exposing the implementation is usually a Bad Thing™.
Until you deal with methods that are either setter or issue the target object to perform some kind of referentially opaque action, the countryside is a happy place with birds happily sings in the background.
When you turn you attention to getters, thundering black clouds start forming overhead and birds are long fled away. With a getter, you expect to call something like
isEmpty() to get something like a
false result. Since actors are just reactive entities there is no such a thing as a message that returns a value (remember the receive function? It was
Akka provides the so-called ask-pattern. This is a mechanism that helps the programmer to set up a send/receive contraption. But the resulting code is pretty actor-aware, and as such it is not applicable to a call method context.
The solution adopted by our fathers was to add methods named like getters that actually sent a query message and let the caller catch the reply if any. E.g.:
def getSize()(implicit sender: ActorRef) : Unit = ref ! QuerySize
First note that the
! operator sends the message and immediately returns, i.e. this function is not blocking.
implicit part transparently captures the actor context when the
getSize method is invoked within an actor. The method implementation needs this reference to send the reply to. If the method is not called within an object then the implementation does not know where to address the reply.
Therefore the problem is only half solved – yes we now can ask only questions that the actor knows the answer, but we have no safety on the returned type and we cannot handle the request outside an actor.
Compare to the Akka solution:
val f = object ? QuerySize
This is untyped, but you get a future that will hold the reply to your request. The future has a timeout that allows you to deal with problems on the other side of the communication (yes, this is halfbaked as well, but seems more general and more reliable). Moreover, the Akka ask pattern doesn’t give a false sense of security.
Actors may exist or not, might be in the proper state to receive and process your message or not. Actors may even be on a different machine so that all the unreliability of the network connection applies.
You could have declared the method to return a future, such as –
def getSize()(implicit sender: ActorRef) : Future[Int] = ref ? QuerySize
But you would have lost the simplicity of the traditional method call/return value all the same. Also, you would have still to deal with impedance mismatch caused by your wrapper.
So far the best approach to the problem I devised is to have an actor subsystem wrapped into a future-based API. The API provides type constraints over the usage of the subsystem. Also the API hides the actor level complexity – users may only reach what they are allowed to. The future abstraction is a fully typed concept that can be used both by actors and non-actors and properly maps the Success/Failure outcome that could arise from a distributed system.
Inside the subsystem, things are quite on a small scale so that the lack of types doesn’t become a too bad hassle.
Lesson Learned – Don’t pretend that actors can be treated like objects, their nature just doesn’t match. Rather than a per-actor wrapping, prefer the encapsulation of an entire actor subsystem in a conventional API wrapper. The use of
Future helps to simplify the integration keeping the right semantic.