Announcements
Today, we're going to start modeling data structures and getting into more complicated computer science. We'll be looking at binary search trees.
What are some examples/properties of binary search trees?
42
20
with left child 10
We've sort of skimmed over the fact that Alloy has a module system. I'm going to take advantage of it today to separate the code over a couple files. There'll be one that defines binary trees and one that looks at descending binary trees.
How might I say what a binary tree is in Alloy? For now, let's only worry about models that have a single tree.
``` module btrees_inclass
sig Node { num: Int, left: Node, right: Node } ```
Q: Shouldn't you say one Node
so that they won't be sets of Node
s?
Tim: Defaults! Defaults always trip me up. Since it's a one-column field, Alloy defaults to one
.
Q: Shouldn't it be lone Node
?
Tim: What happens if we don't make it lone
? Once we rule out cycles, then the tree has to go on forever because
every node has to have a left and right child.
So, we could fix this with lone
, but there's another way. Think about how you'd represent this with algebraic data
types. It'll turn out to be more convenient having a concrete notion of a leaf node.
``` module btrees_inclass
abstract sig Node {} sig Empty extends Node {} sig Data extends Node { num: Int, left: Node, right: Node } ```
Q: Do we want to move the notion of nodes having a value into the Node
field?
Tim: If we do that, then empty nodes also have to also have a value. Empty nodes are sort of a special node that
indicates the absence of something at that child, kind of like null
in Java.
Like in your homework, we'r going to define a predicate that checks if we have a binary tree.
``` pred isBTree { -- root reaches everything some r: Node | Node in r.^(left+right) + r
all n: Node | {
-- not on a cycle
n not in n.^(left+right)
-- at most one parent
lone n.~(left+right)
}
}
run isBTree for 4 ```
Q: Why is it some r
instead of one r
?
Tim: The constraints will turn out to force there to only be one root. one
s are somewhat more expensive for
Alloy, so I'm using the least restrictive constraint I can.
Q: Why isn't it lone r
? What about the empty tree?
Tim: This predicate won't accept empty trees. We're making a conscious decision to exclude them.
Q: For at most one parent, could you also say lone (left+right).n
Tim: Yes, I think so.
Q: Could you talk about what using ~
does here?
Tim: So, left
and right
are binary relations from parents to children. Now, if I flip those around, I'll have
the child-parent relation, so if I join with n
on the left, I'll have all the parents of n
.
When we run this, notice that we can have an instance with just an Empty
node, so we haven't quite ruled out the
empty-tree case. There's also something weird happening with some of our instances. We're getting nodes with the
left and right children being the same!
``` pred isBTree { ... all n: Node | { ...
-- left different from right
n.left != n.right
}
} ```
Adding this makes the spec unsatisfiable. Why?
Student: When we have an Empty
node, n.left
is an empty set since left
and n.right
is also an empty set, and those are equal.
Let's see if Alloy can give us a good enough unsat core. When we adjust the granularity settings, it highlights that
nodes are only allowed one left and one right child, that there can't be cycles, and this constraint that we just added.
That's pretty good, since if we allow cycles then we can just put Data
nodes everywhere and not worry about Empty
.
This version is satisfiable:
``` pred isBTree { ... all n: Node | { ...
-- left different from right
-- could we move this out to no left & right? Yes!
no n.left & n.right
}
} ```
In the Empty
node case, there's no intersection, so it's still ok.
Now we're going to talk about what I've been deferring for the past half hour and deal with the numbers.
``` pred isOrdered { all n : Node, n': n.left.*(left+right) | { n.num > n'.num }
all n : Node, n': n.right.*(left+right) | {
n.num < n'.num
}
}
pred isBSTree { isBTree isOrdered }
run isBSTree for 4 ```
This doesn't work, but we're out of time! Here's a hint: isBTree
is fine, the problem's with isOrdered
.
Q: Don't Empty
nodes not have num
s?
Tim: Exactly! We can show this in the evaluator also. Trying to run our all
constraints there gets some strange results.
Adding - Empty
so that n'
is never an empty node fixes it.
``` pred isOrdered { all n : Node, n': n.left.*(left+right) - Empty | { n.num > n'.num }
all n : Node, n': n.right.*(left+right) - Empty | {
n.num < n'.num
}
} ```