CS1950Y Lecture 8: Modules
Friday, February 9, 2018
From last time, we had a directed, irreflexive graph modeling these roads.
We had trouble writing this isCycle
predicate to find cycles in the roads network. The counterexample we were getting last time had 4 nodes with one in the middle. So this version of the predicate (isCycle3
) is underconstrained - it matches sets of cities that don’t contain cycles.
We’ll try something different this time: using the pigeonhole principle. The pigeonhole principle expresses the idea that if you have n
pigeonholes, and you want to give n + 1
pigeons homes in these holes, then at least one hole must have more than one pigeon in it. In a less bird-centric sense, it means that if you have more objects than bins, then some bin (or bins) must have two (or more) objects in it. How can we use this to find cycles?
If you’re following a cycle along n
nodes, then you’ve got to be able to “escape” - you can’t hit a dead end. So, for a cycle with 4 cities, we need at least 4 edges. For example, if we have cities A
, B
, C
, and D
, we have a cycle with edges A -> B
, B -> C
, C -> D
, and D -> A
.
Do we need more than 4 edges? We don’t! If we had two paths out of a node, for our cycle, we only need to take one of them.
pred isCycle4[cities: set City] {
let localRoads = roads & cities->cities | {
some cycleEdges: set localRoads | { -- subset of localRoads such that...
#cycleEdges = #cities
-- As before, all cities should be able to reach each other. This time, we're using cycleEdges though
all c : cities | all c2: cities - c | c2 in c.^cycleEdges
-- Do we need both of these all's? Can we say something about all subsets of cities?
-- It turns out that, even though that might make logical sense, it's something Alloy has a hard time
-- dealing with, for reasons we'll get to soon.
-- Initially, we had a typo where the predicate took "cities : City" and not "cities : set City"
-- What would happen if we ran this? It would be unsatisfiable, since roads can't have self-loops.
}
}
}
Let’s check if isCycle4
and isCycle3
are equivalent. We’re expecting at least one counterexample though - the problem with isCycle3
(hopefully) won’t show up in isCycle4
!
assert c4eq3 {
some cities : set City |
isCycle4[cities] iff isCycle3[cities]
}
run c4eq3 for 5 City
But is this what we really mean? Do we want just one set of cities where the two are equivalent? We don’t - instead of some
, we should be using all
.
When we run this, Alloy highlights the some cycleEdges
and gives us a weird error message: Analysis cannot be performed since it requires higher-order quantification that could not be skolemized.
We’ll talk about what skolemization is next lecture. It turns out that Alloy isn’t great at quantifying over sets universally, but it can existentially. Think about what c4eq3
desugars to:
assert c4eq3 {
all cities : set City |
isCycle4[cities] implies isCycle3[cities]
isCycle3[cities] implies isCycle4[cities]
}
Now, what does implies
mean? Can we represent it in terms of simpler operators like and
, or
, and not
? There are two cases to consider, for p implies q
: if p
is true, then q
must also be true. If p
is false, it doesn’t matter what q
is - the implication is satisfied either way. Therefore, we can rewrite p implies q
as (not p) or q
.
Let’s apply this to our assertion:
assert c4eq3 {
all cities : set City |
(not isCycle4[cities]) or isCycle3[cities]
(not isCycle3[cities]) or isCycle4[cities]
}
In this not isCycle4[cities]
, what ends up happening? We have something like not some x : A | ...
. The not
can get pushed in though - this is equivalent to all x : A | ...
. Now, this iff
has compiled down to an all
over a set. This is what we just said Alloy is bad at doing! This is where Alloy hits its limit - the methods it solves with don’t scale with this kind of quantification.
We’ll talk about skolemization more, but for now, let’s talk about things that’ll be useful for the homework.
Suppose we have a function like this:
fun evenHops[cities : set City] : set City -> City {
-- pairs of cities in cities connected by 2, 4, 6, 8, ... hops
-- Again, let's use localRoads
let localRoads = roads & cities->cities | {
-- How would we get cities connected by one hop? We can use localRoads - it's already the single hops.
-- How would we get any number of hops? Transitive closure - ^localRoads
-- What about two hops? We can join localRoads with itself - localRoads.localRoads
-- Now, for any even hops. How can we get that? If localRoads.localRoads is all of the two-hops, we can
-- extend that with transitive closure:
^(localRoads.localRoads)
}
}
Now, how can we do odd hops?
fun oddHops[cities : set City]: set City->City {
let localRoads = roads & cities->cities | {
-- Can we do something like this?
-- localRoads.^(localRoads.localRoads)
-- What's true of all odd-length paths? Every odd-length path is an even-length one plus one more hop.
-- To work though, we need to include length-1 hops.
localRoads.^(localRoads.localRoads) + localRoads
}
}
Can we get this by using transitive closure and subtracting out the even hops? Let’s verify it.
fun oddHops2[cities : set City]: set City->City {
let localRoads = roads & cities->cities | {
^localRoads - evenHops[cities]
}
}
Are these equivalent? This is subtle - at first, it sounds like they are. However, we’re not subtracting the trips of even length. We’re subtracting the endpoint pairs of even length. Therefore, there can be some pairs of cities where we can get from one to the other by even-length paths and odd-length hops.
assert oddHopsSame {
all cities : set City | {
oddHops[cities] = oddHops2[cities]
}
}
Why can we do this but we couldn’t run the other assertion? Remember, all Alloy’s doing is looking for instances. If this assertion is true, then that means Alloy finds no instances. When Alloy is looking for counterexamples, it’s trying to find instances of the negation of this. So, we have a not all
that becomes a some
, which Alloy can handle!