Tic Tac Toe in TLA+
Although I've written many informal specifications of software as part of my employments, I haven't ever written any formal specifications (and for good reason: in most domains, the return-on-investment likely isn't there). I like to pretend to myself that given enough time, I probably could write a formal specification, but what's it like to actually do this?
Here I go through the exercise of writing a formal specification of Tic Tac Toe in TLA+ / PlusCal, and then use the associated TLC model checker to see whether an example game is winnable. This felt like just the right level of difficulty to get a feel for writing a formal specification, and had the nice payoff of actually being "useful" for something!
(Below I give code highlights; for the full details see here.)
The game state
We need to start by defining our game state, which involves a couple of TLA+ definitions.
First we need to know what the current board state is. For this, we're going to use a matrix, where the string "atoms" have the obvious meanings:
board = <<
<<"o", ".", "x">>,
<<".", "o", ".">>,
<<".", ".", ".">>
>>;
Secondly, we need to know which player is next to go:
player = "x";
Supporting TLA+ definitions
With the game state defined, we next define a TLA+ formula for what is means
for a player p
to win.
Here \E
and \A
are the existential and universal quantifiers.
winner(p) ==
(* Rows *)
\/ \E i \in 1..3: \A j \in 1..3: board[i][j] = p
(* Columns *)
\/ \E j \in 1..3: \A i \in 1..3: board[i][j] = p
(* Diagonals *)
\/ \A i \in 1..3: board[i][i] = p
\/ \A i \in 1..3: board[4-i][i] = p
We also need to define what it means for there to be an unfilled board space:
unfilled == \E i \in 1..3: \E j \in 1..3: board[i][j] = "."
The game algorithm
Finally, we define our game algorithm in PlusCal,
which will be automatically translated into TLA+.
Note the use of with
for non-deterministic choice
(the model checker will explore all possible instantiations
for us).
while unfilled /\ ~(winner("o") \/ winner("x")) do
with i \in 1..3, j \in 1..3 do
if board[i][j] = "." then
board[i][j] := player;
if player = "x" then player := "o" else player := "x" end if;
end if;
end with;
end while;
assert ~winner("x");
Using the TLC model checker
When we run this from the model checker, we see that it's possible to reach a state where where our "x isn't the winner" assertion fails:
Good luck x
!
Takeaways
(Caveats: I am not an expert, these are my takeaways, this was just a toy example etc.)
I was initially wary of writing the definitions in "math", but it actually ended up feeling liberating not having to define implementation details! For example, I loved that I could write existential and universal quantifiers, know that the model checker would automatically handle the "implementation" of them.
With that said, I liked that I could still write the game algorithm in PlusCal (making use of TLA+ definitions); this gave the game algorithm a familiar, imperative feel. Which is exactly the point of PlusCal! It was also useful from the point-of-view of learning TLA+ to hit Command+T and see the corresponding TLA+ next-state relation.
I can imagine eventually writing everything in TLA+, but for now I'll probably stick with a hybrid TLA+ / PlusCal approach.
Resources
- TLA+ lectures; The first four of Leslie Lamport's lectures are a great introduction to writing your first TLA+ spec. They're pretty short, and I found them laugh-out-loud funny.
- Learn TLA+; gives a good introduction to PlusCal.
- A PlusCal User's Manual; gives the details of PlusCal.
- How Amazon Web Services uses formal methods; some examples of when it makes business-sense to use TLA+.