CS1950Y Lecture 13: Modeling Data Structures 2
February 23, 2018


Finishing up Linked Lists

Why would we model a linked list data structure in Alloy? What’s the point?

You can prove properties of the list! We added this event modeling the list function that adds to the head of the list, and now we can show that invariants hold.

Do you still need to test your list code? Absolutely! There could be a bug in the implementation that isn’t reflected in the Alloy spec.

This raises another question. If we still need to test our code after modeling, what’s the point of modeling? Modeling shows that we understand the problem correctly and that our algorithm works. We can think of this as two separate things:

  1. Is the thing we’re trying to implement correct? (Alloy)
  2. Did we implement it correctly? (Testing)

Neither testing nor modelling replace each other - we still get value out of both, and having multiple checks makes us even more sure of what we’re doing.

There are a couple subtle points about the event from Wednesday to touch on:

Recursion in Alloy

Today, we’re going to model Boolean logic in Alloy. We won’t go through the traditional, rigorous, whiteboard definition of a logic. Instead, we’ll do it in Alloy!

Let’s start with defining formulas:

abstract sig Formula {

}

This is an abstract sig, so it’ll be the union of all of its formulas. Now, we need to add some types of formulas. What’s the simplest type of formula? It’s a single variable. We can then inductively define other types of formulas. For example, if p and q are Boolean formulas, then p and q is also a formula. These formulas defined in terms of other formulas will have properties that are themselves formulas:

sig Var extends Formula {}
sig Not extends Formula { child: Formula }
sig And extends Formula { left, right: Formula }
sig Or extends Formula { left, right: Formula }

-- We'll see shortly why we're adding integers
run GiveMeAFormula { some Formula } for 5 Formula, 5 int

Let’s see what instance we get. Does it seem like a well-formed-formula?

So, we have an or. It has a left side and a right side, and they’re the same. Both are an and. What’s the left side of that? It’s a variable - we can deal with that. But the right side is that same and! This clearly can’t be well-formed - we’ve got some kind of strange, infinite formula.

Let’s fix this:

fact wellFormed {
    -- We're using the old computer science trick of assuming a function that does what we want and then worrying about
    -- how it works later.
    all f : Formula | f not in allSubformulas[f]
}

fun allSubformulas[f : Formula]: set Formula {
    -- We have all of these different kinds of child relationships, so how can we traverse them all?

    -- We have two different left and right relations, for and and for or. These have to be disambiguated somehow,
    -- or Alloy won't know what to do with it
    -- f.^(child + left + right + left + right)

    -- The <: is what Alloy calls the domain restriction operator and what we prefer calling the alligator. It lets us
    -- restrict a relation to only things where the left side of the relation is in a particular set (like all the
    -- Ands)
    f.^(child + (And <: left) + (And <: right) + (Or <: left) + (Or <: right))

    -- But how does this work? Depending on the type of formula, it might not have a left and a right. When we do transitive
    -- closure, that'll just do all the joins that it can. Then, when we do the relational join, it'll evaluate to the empty
    -- set for things that don't match up. For example, if f is Var$0 and the transitive closure comes out to
    -- { And$0 -> Var$0, And$0 -> Var$1, Or$0 -> And$0, Or$0 -> Var$0 }, then the dot join of those will just be {}. 
    -- To get this relational join, we could have started with something like this:
    --      And <: left = { And$0 -> Var$0 }
    --      And <: right = { And$0 -> Var$1 }
    --      Or <: left = { Or$0 -> And$0 }
    --      ...
    -- Then, the transitive closure will keep joining those wherever the atoms line up.
}

Supposed I’d like to find the size of a formula, as given by the number of parts/subformulas in it.

Something like this is really appealing:

fun size[f: Formula]: Int {
    1 + #allSubformulas[f]
}

But allSubformulas returns a set, which can’t have duplicates. So if we have a formula that refers to the same formula multiple times, it only gets counted once.

What we really want is recursion in Alloy, without having to do all sorts of workarounds. This is a general technique that’s really helpful for Alloy specs. Alloy also has very limited recursion support, but it doesn’t work all that well and you shouldn’t trust it. Essentially, we’ll use a constrained field that represents the recursive value we’re trying to compute.

We’ll start by adding a size field to Formula:

abstract sig Formula {
    size: Int
}

What constraints do we need to make this true? size should be 1 + the size of the subformulas. The constraint on size will depend on what kind of formula we have:

sig Var extends Formula {} {
    size = 1
}

sig Not extends Formula { child: Formula } {
    -- We need to use @ because, within a sig fact, there's always an implicit `this.` in front of any name that's a field
    -- of the sig. For example, here `size` is the size of this particular `Not` formula, not the overall `size` relation.
    -- Sig facts are really nice and can make your code much more concise, but if you end up using the Twitter operator
    -- (what Tim calls it) a lot, you might be better off using a fact with an explicit `all`.
    size = add[1, child.@size]
}

sig And extends Formula { left, right: Formula } {
    size = add[1, add[left.@size, right.@size]]
}

sig Or extends Formula { left, right: Formula } {
    size = add[1, add[left.@size, right.@size]]
}

Helpful tip: when displaying instances, we can go into the theme settings and show the size as an attribute of the atom instead of an arc.

The takeaway here is that you cannot have traditional recursion in Alloy, because Alloy has to turn the spec into a Boolean formula. You could spell out the recursion up to a certain depth, but that doesn’t scale well. Instead, you lift the recursion up into the spec, sort of like skolemization. This forces Alloy to respect the recursion relation as part of the spec without requiring any recursive execution.