It’s pretty well known that using VARs is, apart from unethical, the evil itself, some kind of hell, it make kitties die and many other stuff you might probably heard before and that could eventually be the cause of a painfull slowly dead.
The essence of functional programming is therefore the immutability: every time I mutate an element, I actually generate a new one.
What about Akka actors?
When we talk about actors, we can define them as stateful computation units that sequentially process a message queue by reacting(or not) to each of these messages.
It’s always been said that, in order to keep state within some actor’s logic, it was ok to use VARs:
It’s impossible that concurrency problems happen: it’s the actor itself and nobody else who access that var and will only process one message at a time.
But maybe, we could renounce to this premise if we look for some way to redefine the actor’s behavior based on a new state.
Mortal approach
If we follow the previously described philosophy, the very first (and more straight forward) approach for keeping some actor’s state would be pretty similar to the following:
class Foo extends Actor{ var state: Int = 0 override def receive = { case Increase => state += 1 } }
Every time an Increase
arrives, we modify the state value by adding 1.
So easy so far, right?
Immutable approach
Nevertheless, we could define a receive
function parameterized by certain state, so when a message arrives, this parameter is the state to take into account.
If the circumstances to mutate the state took place, we would just invoke the become
method that would modify the actor’s behavior. In our case, that behavior mutation would consist on changing the state value.
If we use the previously defined example:
class Foo extends Actor{ def receive(state: Int): Receive = { case Increase => context.become( receive(state + 1), discardOld = true) } override def receive = receive(0) }
we can notice that the function defined by receive
is parameterized by some state
argument. When some Increase
message arrives, what we perform is an invocation to become
for modifying the actor’s behavior, passing as an argument the new state to handle.
If we wanted some extra legibility, we could even get abstract from every updatabe-state actor:
trait SActor[State] extends Actor { val initialState: State def receive(state: State): Receive def update(state: State): Receive = context.become( receive(state), discardold = true) override def receive = receive(initialState) }
this way, we just have to define the initial state of some actor, a new parameterized receive
function and a new update function that takes care of performing the proper become
call as explained before.
With all these in mind, we now have some cuter brand new Foo
actor:
class Foo extends SActor[Int] { val initialState = 0 def receive(state: Int): Receive = { case Increase => update(state +1) } }
Potential hazardous issues
Please, do notice that in the featuring example, we’ve used a second argument for become
: discardOld = true
. This argument settles whether the new behavior should be stashed on the top of the older one, or ‘au contraire’ it should completely substitute the previous behavior.
Let’s suppose we used discardOld = false
. If every single time a new Increase
message arrived we had to stash a new behavior, we could obtain a wonderful overflow issue.
See you in the next tip.
Peace out 🙂