Akka provides a nice framework for implementing a finite state machine with it’s FSM
trait. In this post we’ll take a look at some of the features that FSM
provides for an actor. The main documentation for this feature is in the Akka documentation, while the API is in the scaladoc. The sample code for this post can be found at scala-samples/akka-fsm.
State and Data
As with any finite state machine, an Akka FSM
actor is modeled around the concept of state. The actor must be in one of the defined states at all times. States in FSM
are modeled in the type system. Typically an FSM
implementation will define a common trait and have a case object for each state that extends the common trait. If we’re building a tic-tac-toe game, we might define the states below.
trait GameState case object PlayerXTurn extends GameState case object PlayerOTurn extends GameState case object GameOver extends GameState
In addition to state, Akka FSM
allows us to attach a data object to each state. As with state, it is common to define a trait and then have case objects or case classes extending that common trait. We could have different types for each state in our FSM, more data types than states, or whatever makes sense for the actor. In this case, we’ll just define our data type as a Map[Int, Option[Player]]
. The keys of this map will be numbers representing the squares (numbered 0-8, left to right), while the values indicate which player is occupying the square. The value will be None
if there is no player in the square, or Some(X)
or Some(O)
f when one of the players has occupied the square.
Once types are defined for the state and data, we can mix in FSM
to the Actor. FSM
must be parameterized with the types we’ll be using for state and data.
trait Player class Game extends Actor with FSM[GameState, Map[Int, Option[Player]]]
The last thing to do is to define what state the FSM
should start at and the data at that state. That is defined with a call to the startWith
method in the actor body. In this case, we’ll start at PlayerXTurn
with an empty game board.
class Game extends Actor with FSM[GameState, Map[Int, Option[Player]]] { startWith(PlayerXTurn, Map.empty ++ (0 to 8 map(i => i -> None))) }
Reacting to Events
Once the an actor starts, it’s probably going to be getting some messages. In a plain Akka actor, any object can be sent to the actor as a message. An FSM
actor is no different, but the messages are wrapped in an instance of Event
, which also provides the current state data. The way that we react to a message will likely be different depending on what state the FSM
is currently in. The when
method is used to define behavior for a specific state of the actor. The code below show the start of defining behavior to be applied when the FSM
state is PlayerXTurn
.
The second argument to when
is a PartialFunction[Event, GameState]
, which effectively means that we need to define a function that accepts Event as an input and returns GameState. Partial functions are typically implemented with a pattern match as shown next.
The curly braces now contain a pattern match on Event
. The first case matches when an Event
is received where the message is PlayerXMove. We bind the requested square to squareId
and the current state to gameBoard
. The pattern is further restricted to only match if the requested square contains None
, meaning that it is not occupied. The second case is a wildcard match, so it will match on anything not matched by the first case. This case handles the case that we get a PlayerOMove
message or any other message.
If we have a valid PlayerXMove
, then the state machine should transition to PlayerOTurn
and the game data at that state should be updated to inlude X’s move. FSM
provides the goto
method, which takes a state as it’s argument. Often goto
is followed by a call to the using
method, to which we pass the data for the state.
The call to replying
on line 6 allows us to send a message back to the sender of the PlayerXTurn
message. Here we are sending the updated game board back to the sender.
When the wildcard case matches, an invalid message has been sent to the actor. We don’t want to change state or update the game data, but we will send back InvalidMove
to the sender.
The stay
method is invoked to say that the Event
has been handled but we don’t want to transition to a new state.
The game actor will have another when
call that defines the behavior for PlayerOTurn
, which will be very similar to the behavior for PlayerXTurn
.
Finally, we’ll have a when
call that defines the behavior in GameOver
state to simply return the final game board to the caller.
Testing an FSM Actor
Of course, we must be able to write tests for our FSM
actor. Akka provides nice testing support via it’s TestKit
infrastructure, which includes some FSM-specific support as well. The first step in testing an FSM
actor is to create an instance of it with a call to TestFSMRef
, which will return an ActorRef
. This actor, however, is a synchronous actor that’s specifically designed for unit testing. Unlike a normal actor, it behaves synchronously and exposes internal state. The stateName
and stateData
properties let us verify that the FSM
actor is behaving the way we expect.
Any messages sent to the actor are processed synchronously. When we send a message with ask
, the message is processed and the Future
returned is already completed. Again, this is all to make unit testing easy and is not the normal behavior of an actor.
class GameSpec extends TestKit(ActorSystem("game-spec-system")) with FlatSpecLike with Matchers with ScalaFutures { val EmptyBoard = Map.empty[Int, Option[Player]] ++ (0 to 8).map(i => i -> None) "GameActor" should "allow X player to move first in new game" in { val fsm = TestFSMRef(new Game) fsm.stateName shouldBe PlayerXTurn fsm.stateData shouldBe EmptyBoard fsm ! PlayerXMove(4) fsm.stateData shouldBe EmptyBoard + (4 -> Some(X)) } }
The test above begins by creating an instance of the FSM
actor class Game
and assigning the ActorRef to fsm
. It then verifies that a newly created game starts in the state PlayerXTurn
and that the data in this state is an empty board (one where no square is occupied). It then sends a PlayerXMove
message to the actor saying that X
would like to occupy the center square, and then checks that the game board is updated to show Some(X)
in square 4.
Wrapping Up
We’ve walked through a quick introduction to using the FSM
trait in Akka. I think this trait provides a nice framework for implementing an actor in terms of states and transitions. This example is quite simple, and so it may seem like overkill in this case. I think the value of the FSM
trait lies in making the concepts of state and state data explicit. The ability to test an FSM
actor’s current state and data is also a nice feature, which I think may be harder to do with alternate patterns.
A more complex FSM actor can be seen in the reactive-quizzo sample code in Game.scala