Introduction to Pragmatic Formal Modeling
- Introduction
- The modeling and model testing language
- I want to run the examples without looking at the learning material!
- What does modeling get us? (A Simple Example)
Introduction
A formal model is a mathematical description of a system, generally to be implemented in hardware or software. They are useful for two reasons. Firstly, mathematics, unlike English, is precise and unambiguous. Even if this is where you stop, it forces you to understand the system you are describing. Secondly, mathematically based models can be checked. You can describe success criteria, and if they are violated you can see the exact series of steps that led to that. This is particularly useful for distributed systems, from multiple threads on a computer to thousands of computers in a cloud service.
When you think about formal modeling, it’s easy (and intimidating) to jump straight to the most complex use cases. Sure, those algorithm geniuses who design the fundamental algorithms of distributed systems might need it, but what about me? I care about the quality of my work. But I work in industry. Maybe I work in the cloud, coordinating microservices. Maybe I’m a game developer writing the next multiplayer networking library. Maybe I’m building a peer to peer file storage solution. Regardless, the question remains:
How can I model the system I’m building in a way that’s useful, practical, and has good ROI for myself and my company?
For every example on this site, we’re going to try to take a pragmatic approach that mirrors the engineering design lifecycle. We’ll start with UML diagrams and relatively precise descriptions, and then convert them into a formal specification language. Then we’ll see how we can check for design errors, and get concrete examples as to how they occur. Finally we will show how to use the detailed model errors to progressively refine your designs.
The modeling and model testing language
TLA+ is a specification language developed by Leslie Lamport, one of the computer science greats. It has a model checker: TLC, which works in a brute force manner to check every possible state of your modelled system.
TLA+ is used in industry. AWS is one of its most enthusiastic users, along with Microsoft and Intel.
While it looks very math-y, it is surprisingly accessible and practical. TLA+ was designed as a tool for engineers, not just algorithmists. But current examples tend to fall into one of two categories: toy problems, or complex algorithms. One of my goals with this site is to present examples that show how it can be used in an engineering process for “normal” engineers.
My advice is to skim the examples and see if they resonate. Could this help you in your day-to-day? Does it pique your interest? If so, go through the learning material and revisit this post.
However if you are mathematically inclined and obsessed with complex distributed system algorithms already, I’d advise you just jump straight to the TLA+ Video Series: Leslie Lamport will be able to sell you better than I can.
I want to run the examples without looking at the learning material!
Ok, here’s how: First download VSCode TLA+
For each example you want to run:
- Click Download Code. The file will look like “modelfoo.tla”
- Scroll down (or hit Next Section), where you will see a Download Configuration link. Click it. The file will look like “modelfoo.cfg”
- Place both files in the same folder. Make sure they have the same name (other than the extension). If necessary rename “modelfoo_small.cfg” to “modelfoo.cfg”
- Open the folder in VSCode and open the TLA file
- Right click inside the editor and click Check Model with TLC
- An output window will open and you will get a result. Or you will get an error, probably because you didn’t rename the .cfg file
For any example that relies on additional model files, simply download them and place them in the same folder before clicking Check Model with TLC.
What does modeling get us? (A Simple Example)
We will start with a toy problem, Tic-Tac-Toe, just to demonstrate some of the core benefits of modeling and model checking. This is the only toy problem, so feel free to skip ahead to the practical part. If you want to get a bit more of an intuitive understanding, keep reading.
The Model
I advise that you look at the code both in LaTex and code form. The LaTex is generated directly from the code and can let you appreciate the math a bit better.
Essentially, this model describes every game of Tic-Tac-Toe that could be played, including games where someone has already won, but the board isn’t full. The model checker actually shows us how many possible board/move combinations exist:
State Name | Total States | Distinct States |
---|---|---|
Init | 1 | 1 |
MoveX | 9963 | 2907 |
MoveO | 9144 | 3138 |
It ends when there are no more spaces left to fill. None of the Next actions are activated, so the system “deadlocks.” Sometimes this behavior is an error, but for us it’s a feature. You can configure how to treat it.
Checking the Model
All right, that’s kind of cool, but so what? Say that it would be really bad if O were to win. We can check a model against the OHasNotWon invariant to make sure it’s impossible for O to win. Let’s run that now.
Oh no, it is possible for O to win. Invariant OHasNotWon is violated.
- 1. Initial predicate
- board
- : ___
- : ___
- : ___
- nextTurn
- : X
- board
- 2. MoveX
- board
- : X__
- : ___
- : ___
- nextTurn
- : O
- board
- 3. MoveO
- board
- : XO_
- : ___
- : ___
- nextTurn
- : X
- board
- 4. MoveX
- board
- : XOX
- : ___
- : ___
- nextTurn
- : O
- board
- 5. MoveO
- board
- : XOX
- : _O_
- : ___
- nextTurn
- : X
- board
- 6. MoveX
- board
- : XOX
- : XO_
- : ___
- nextTurn
- : O
- board
- 7. MoveO
- board
- : XOX
- : XO_
- : _O_
- nextTurn
- : X
- board
Now the interesting thing is not that O can win Tic-Tac-Toe. We probably knew that. But the model checker uses Breadth-First-Search, so not only is this a possible O victory, there are no faster victories. What’s also interesting is how clearly it is presented. The data above is EXACTLY what comes out of the model checker, prettied up with generic css and a bit of annotation. Look how much clearer it is than a standard code debugger. Because we’re not debugging code, we’re debugging logic.
Let’s see what happens when X wins.
X can also win. Invariant XHasNotWon is violated.
- 1. Initial predicate
- board
- : ___
- : ___
- : ___
- nextTurn
- : X
- board
- 2. MoveX
- board
- : X__
- : ___
- : ___
- nextTurn
- : O
- board
- 3. MoveO
- board
- : XO_
- : ___
- : ___
- nextTurn
- : X
- board
- 4. MoveX
- board
- : XO_
- : X__
- : ___
- nextTurn
- : O
- board
- 5. MoveO
- board
- : XOO
- : X__
- : ___
- nextTurn
- : X
- board
- 6. MoveX X can win in 5 moves, because X goes first
- board
- : XOO
- : X__
- : X__
- nextTurn
- : O
- board
Finally, let’s look at a stalemate. In those past wins, it looked like the other player wasn’t trying very hard. That’s because we saw the fastest possible wins. Let’s see a potential stalemate:
Stalemate is also a thing that can happen in Tic-Tac-Toe. Invariant NotStalemate is violated.
- 1. Initial predicate
- board
- : ___
- : ___
- : ___
- nextTurn
- : X
- board
- 2. MoveX
- board
- : X__
- : ___
- : ___
- nextTurn
- : O
- board
- 3. MoveO
- board
- : XO_
- : ___
- : ___
- nextTurn
- : X
- board
- 4. MoveX
- board
- : XOX
- : ___
- : ___
- nextTurn
- : O
- board
- 5. MoveO
- board
- : XOX
- : O__
- : ___
- nextTurn
- : X
- board
- 6. MoveX
- board
- : XOX
- : OX_
- : ___
- nextTurn
- : O
- board
- 7. MoveO
- board
- : XOX
- : OX_
- : O__
- nextTurn
- : X
- board
- 8. MoveX
- board
- : XOX
- : OXX
- X is about to win : O__
- nextTurn
- : O
- board
- 9. MoveO
- board
- : XOX
- : OXX
- O must block : O_O
- nextTurn
- : X
- board
- 10. MoveX So that a stalemate occurs
- board
- : XOX
- : OXX
- : OXO
- nextTurn
- : O
- board
So O blocked an X win here, but there is no intelligence yet. There is another world where O didn’t block X, and it was an X win. This is just one of the possible stalemates. So in our current system it will be possible for O to win. What can we do to fix that?
Playing not to lose
Let’s imagine it is really important that O never win. If they do, the casino you work for loses millions of dollars (why they introduced high-stakes Tic-Tac-Toe is above our pay grade).
O will still play every possible game available to it. But the casino is X, meaning we can change its strategy. How do we do that? We put stricter limits on what is considered an allowable move for X:
- The previous version of MoveX let X be put into any unfilled space.
- In this updated version, a programmer read the WikiHow on “How to Play Tic-Tac-Toe” and made a best attempt at a strategy.
We need to prove the O will not win if this strategy is used. We do this by encoding it to TLA+ and running the same check (to see the full implementation, click Download Code or Download PDF):
Let’s see what happens when we set it up with invariant OHasNotWon:
State Name | Total States | Distinct States |
---|---|---|
Init | 1 | 1 |
MoveX | 800 | 382 |
MoveO | 648 | 488 |
We didn’t get an error trace! Normally that’s a cause for suspicion; did we mess up the test? But by building incrementally we can have more confidence: we’ve seen this test fail. So we did it! O wil never win. So what exactly does that mean?
The strategy isn’t optimal (i.e. wins in the smallest number of moves) or deterministic. For any given turn it may allow multiple moves. The TLC model checker tested all of those moves and ensured that O would never win, no matter which of those moves we pick. This is true no matter what O does. We will call all moves allowed by the strategy Strategy Moves.
If we wanted to maximize winning, we could run a machine learning algorithm that tries to predict which move would lead to victory. It doesn’t even need to be completely logical, it could try to psych out its opponent, or realize that people wearing red shirts are more likely to move left. As long as it only picks moves that are Strategy Moves we will never lose.
This is a silly example. But combining machine learning with logical safeguards designed and tested with TLA+ has a lot of potential. TLA+ is logical modeling; it can’t give you statistical optimization or tell you how many 9s of reliability you will get. But it can let you update algorithms confidently, knowing that critical parameters will be met.
Playing to win
“So you stopped us from losing,” says your boss, “but that was yesterday, and anyway, we’re a company of winners.” “Right on it boss,” you respond, because you know what to do next. To prove that our system is going to win, we have to first describe winning. Let’s start with what we already know.
But at the start of the game you haven’t won. This would fail. We need a new operator.
This is what’s called a Temporal Property, a property that is measured across steps, and we can test for it. X eventually winning sounds exactly like what we want.
If a little thing called “Eventual Consistency” is important to you, don’t worry, you’ll see this again!
So let’s test our code.
We get an error, but it’s unlike any we’ve seen before. What is a Stuttering Step? Well, so far we’ve been thinking about this one state after another. But another valid thing that can happen is nothing. And nothing can happen forever. There’s a world where one of the participants just walks away from the game board, which means X will never win. How cool is it that our modeling tool can point that out to us?
But don’t worry, our casino won’t allow that to happen. If a player can take a move, they will eventually take a move. We say that formally with the concept of Fairness. Weak Fairness (represented as WF) roughly means that if a move can be made, and that fact doesn’t change, the move will eventually be made. We define it below.
Note: Fairness isn’t something we are checking for, it’s a property of how the system works.
Great, let’s run it again.
We run into a stalemate. Which means that X won’t always eventually win.
- 1. Initial predicate
- board
- : ___
- : ___
- : ___
- nextTurn
- : X
- board
- 2. MoveX
- board
- : X__
- : ___
- : ___
- nextTurn
- : O
- board
- 3. MoveO
- board
- : XO_
- : ___
- : ___
- nextTurn
- : X
- board
- 4. MoveX
- board
- : XO_
- : _X_
- : ___
- nextTurn
- : O
- board
- 5. MoveO Click here to see X in a interesting position
- board
- : XO_
- : _X_
- : __O
- nextTurn
- : X
- board
- 6. MoveX then click here to see X set up the win
- board
- : XO_
- : _XX
- : __O
- nextTurn
- : O
- board
- 7. MoveO which is sadly foiled
- board
- : XO_
- : OXX
- : __O
- nextTurn
- : X
- board
- 8. MoveX
- board
- : XOX
- : OXX
- : __O
- nextTurn
- : O
- board
- 9. MoveO
- board
- : XOX
- : OXX
- : O_O
- nextTurn
- : X
- board
- 10. MoveX While a stalemate still occurs, look how much harder X fought
- board
- : XOX
- : OXX
- : OXO
- nextTurn
- : O
- board
So we didn’t succeed. X does not always eventually win. The house may not always win, but it never loses. We can live with that. But if you figured out an algorithm to make X always win, this is a way to prove it.
Tic-Tac-Toe is a solved problem, and there is no such algorithm, but don’t let that limit you.