CS1950Y Lecture 4: Relational Expressions
Wednesday, January 31, 2018
Let's continue from last time. We were making a model — what's a model in this context?
It's something where we've made explicit abstraction choices. We're trying to capture the essence of the thing and use that to capture useful things about the real-world system or program. It can't be too detailed, because that's not helpful, but it has the same constraints as the real thing. We can run it over and over again without consequences, like modeling an earthquake as opposed to creating real earthquakes to test a geophysical theory.
We left off in our Tic-Tac-Toe model adding a fact that there's at most one mark per square. Remember, without this Alloy is free to give us boards with any number of marks per square. That's one of its strengths - showing us instances we didn't expect to show incorrect assumptions.
To model X's turn, we use a few new Alloy constructs. One is the set builder notation { r, c : Index | b.places[r][c] = PX }
, which finds tuples r -> c
where r
and c
are in Index
such that the board has an X at that location.
The other is the counting operator #
, which counts the number of elements in the set it's in front of.
Now, how can we model O's turn? How about not xturn[b]
? This won't work, because it assumes a valid board — if someone
cheated, we could get a board where it isn't X's turn, but it also shouldn't necessarily be O's turn either. Another
issue is that at the end of the game it's no one's turn.
We need arithmetic! When it's O's turn, there's one more X on the board than O's. To capture this, we'll use Alloy's
sub
function. When it's O's turn, the number of X markers minus 1 should equal the number of O markers.
Next, let's model someone winning the game. There are 3 ways a given player can win: horizontally, vertically, and
diagonally. We can represent each way of winning with a separate predicate and then or
them together in our winning
predicate. Note that this is the same kind of abstraction that you'd use in a program to keep your code readable.
One way to do this would be to explicitly write out the constraints for each row. Alloy lets us be more concise
though — we can quantify with some
to say that there's some row such that our player wins on it. This is a bit more
powerful than a programming language where we have to iterate over all the possibilities.
Our model is still missing something — we have no representation of a move happening. We're not going to do this
by constructing the next board, b'
, because that's not something we can do in Alloy. Instead, we'll constrain valid
next boards, defining what a valid transition between boards looks like.
In our transition predicate, the arguments correspond to a move by player p
at (r, c)
where board b
becomes board b'
. What had better be true about a move?
- There wasn't already a marker there:
no b.places[r][c]
- The player moves into that spot:
b'.places[r][c] = p
- It's actually the player's turn
- The rest of the pieces don't change. Formally, this is called a frame condition, meaning that there are no changes other than the ones we're interested in.
How do we write this frame condition? One way would be to say that for all indices
other than (r, c)
, things are the same between b
and b'
. That sounds like a
lot of work. Instead, let's constrain the whole relation. We'll replace the line b'.places[r][c] = p
with this: b'.places = b.places + (r -> c -> p)
. This just adds the new row to the previous relation!
Coming from other languages, this seems pretty weird.
How might we write this in other languages? It's tempting to do something like this:
b'.places[r][c] = b.places
b'.places[r][c] = p
This won't work! We can't actually mutate b'
like this in Alloy. Everything is immutable, and order doesn't
actually matter. What we're doing in Alloy is describing a system of constraints, so it doesn't matter what order the
constraints go in. Implicitly, everything in a predicate is and
ed together, and and
is
commutative.
Do we need to constrain that the b
board is valid? We've made the modeling decision that the
move
predicate is defined on any board, even cheating ones. Eventually, we'll be modeling
whole games, where we know we have a valid starting board. Since there's a move transition from each board to the next,
this means we'll go from valid board to valid board.
Someone asked if we need to constrain that who's turn it is changes after a move. Because we're defining this, we don't need to add a constraint, but we can try to find a counterexample to check.
pred counterexample {
some b, b' : Board, r, c : Index, p : Player | {
move[b, r, c, p, b']
(xturn[b] and xturn[b']) or
(oturn[b] and oturn[b'])
}
}
We're looking for some valid move, and then using our xturn
and oturn
predicates to see
if it was the same player's turn before and after. When we run this, Alloy can't find any instances, so it seems like
our model is sound — it conforms to our expectations. Since we're talking about a bounded universe of 3 indices
and 2 players, this is an exhaustive check. Unlike tests in a programming language, Alloy can confirm that, within our
universe, there are absolutely no counterexamples at all. If our model were off, and Alloy did find a counterexample,
it'll even show us an instance that shows what happened.
When we're running a predicate, we'd like to know it's either satisfiable, with an example, or unsatisfiable, with a proof of why. Alloy won't exactly give us a proof, but it can give us an unsatisfiable core — a set of clauses that seem to lead to the contradiction.