My 2cts. It looks like you come from the developer world and seem to go through the same struggle as I am going through. So here are my personal experiences.
I was always desperately looking to apply my extensive programming knowledge & idioms so the Alloy code made sense. I over-used the quantifications because they felt so familiar as loops. But they arenāt. It is a bit like driving a plane as if it is a car.
I had not seen this message first, so as you said I answered on SO. The model I posted there was more concise. Now, this is about style issues so reasonable people can differ in opinions. One should always follow the style one is comfortable with. However, when you learn to fly, it does help to realize that you steer with your feet.
So I just like to tell the reasons why I did things differently since I tend to think very deeply about this and I am probably a few years ahead in the flying.
So this was the model I came up with (I modified the unchanged part)
enum LockState { Locked, Unlocked }
sig Door { var state: LockState }
sig Vehicle { doors : disj some Door }
pred Door.unlock { this.state' = Unlocked }
pred Door.lock { this.state' = Locked }
pred trace {
Door.state = Locked
always (
some d: Vehicle.doors {
(d.unlock or d.lock)
unchanged[state,d]
}
)
eventually Door.state = Unlocked
}
run trace for 4 but exactly 2 Vehicle, exactly 4 Door
pred unchanged[ relation : univ->univ, exclude : set univ ] {
(relation - exclude->univ)' = (relation - exclude->univ)
}
Getters/Setters
You added āmethodā predicates for accessing the state
of a Door
and keeping it unchanged. In a large program, this is really good practice. However, one of the powers of Alloy is that all values are global. I.e. you can navigate from a Door to a state
doors.state -- all states in use
But also from a state to the doors:
state.Unlocked -- all unlocked doors
Now donāt get me wrong. It took me a long time to feel that Door.state = Unlocked
is identical to all d : Door | d.state = Unlocked
. The second form feels so familiar. However, now Iāve learned to fly a bit, the first form is much more readable.
One could say that all d : Door | d.state = Unlocked
is easier to read for newcomers but I think that is only true for newcomers from the procedural world. And they need to get the feel that Door.state = Unlocked
is the same as fast as possible since it would reduce a lot of the Alloy power if you wrote models without them. I think.
By abstracting away the fields/relations I think you reduce the power of Alloy enormously. One of the hard parts of learning Alloy was for me to say goodbye to the OO model with private fields and embrace the relational model where data is completely unprotected. To understand that a relation is a global thing is something I can still struggle with.
In OO we hide our data, in Alloy (and relation) the data is what we work with. I actually also concluded in the procedural world that data is way more important than the OO gurus taught us. I work in the Java world and all these trivial getters and setter methods can drive me nuts since they rarely ever provide value.
When you use getters and setters youāre almost everywhere forced to use quantifications instead of relational. And imho worse, the name of the getters and setters cannot be trusted so to understand a model I need to look at their content. However, they most often consist a single simple Alloy expression that is often more concise and informative than a name that expresses only an intention.
For example you have
pred unchanged[d: Door]{ d.state = d.state'}
To me, d.state' = d.state
screams unchanged state for that door. Actually, it tells me that only the state of the given door is unchanged, something the name does not convey to me.
I do not think that when your model grows your unlock
, locked
, unchanged
, etc. predicates will become larger because they do not abstract anything. You cannot hide their underlying relation so you need to police your code to never use the relations directly. I think that will explode your model with predicates that are way more complex than what they abstract. The reason we use in OO of privacy and cohesion do not really apply here.
So I think it is better to stay away of the getters and the setters. In OO, the methods abstract the data. In Alloy, the data is all you got.
I tend to start with the data only. Only when I find that I repeat a non-trivial sequence of Alloy statements do I tend to put it in a predicate or function. And then as always I tried to make them reusable in multiple contexts. For example, the following unchanged
method works for any relation:
pred unchanged[ relation : univ->univ, exclude : univ ] {
(relation - exclude->univ)' = (relation - exclude->univ)
}
In my mind trying to abstract the sigās data is very much working against what makes Alloy so powerful. But your mileage may vary of course.
The reason I did not remove the lock
and unlock
predicates is that they are the action predicates. In my experience, they will grow in complexity over time.
OO like syntax
Not that important. However, if a predicate belongs to a type, I like to use the OO syntax. I.e.
pred Door.lock { ... }
Instead of
pred lock[ d : Door ] { ... }
In Alloy, they are both allowed. With the OO style syntax, there is a this
keyword. Sadly you do not get an automatic resolution to the fields of names, one of my wishes is to extend the syntax to define these predicates inside the sig
. Then again, it is only syntactic sugar.
Daniel doesnāt use the OO syntax in his book very often but I really like it. Because it is not in the book it is not well known.
Global/Modularity
One of the things I am still struggling with is the modularity in Alloy. In my experience so far, models tend to be quite small and most examples Iāve seen use many a fact
to constrain the model.
I do not like facts since they are global, you cannot selectively enable/disable them, unlike predicates. (And there is no difference between them, they are internally identical.)
One of the reasons I tend to use predicates instead of facts is when I gradually want to explain things. Alloy has also a markdown mode when you start a file with a YAML header
This files can be edited in Alloy as markdown. Only the Alloy code blocks are given to the analyzer. This approach allows you in the run
command to specify a specific predicate or provide a body.
This allows you to build up a model part by part and show run
command that exercise each part separately, usually in order of complexity. However, any global fact is, well, ehh, global. If you combine modules, a stray fact can easily get in you the dreaded state of not finding a model.
That said, Iāve people tell me they donāt like it, they prefer to treat the file as their scope.
Granularity
In my model, the trace
predicate contains the three predicates that you made explicit by giving them a name. It is similar to my comment on getters and setters. These predicates are rarely reusable in my experience, their only purpose is to have names. However, I need to look all over the file to find out what begin
and targetState
really mean since names can be wrong.
In the trace
predicate the scope is clear, weāre setting up a trace. When I see:
Door.state = Locked
Then I know a lot more than when I see
begin
As a developer I applaud you but as a modeler I am always torn between the cohesive model of having a method/predicate do one thing only and the advantage of having it all visible in one place. In this case, I tend to keep them together in one predicate.
This puzzled me because I am a sucker for this cohesion rule. I think the rationale is that Alloy is so much more powerful than a programming language. A single line can do many lines of code. And if you use the proper names for the atoms and relations you can make it really readable.
Another reason might be that one of the hardest problems I experienced in Alloy is the dreaded situation that you do not get an instance. Somewhere, there is a rule that makes an instance impossible. Once you have an instance, you can use the evaluator to explore things and our mind is brilliant in seeing what is wrong.
However, that is all moot without an instance. So quite often you need to tune the model by commenting out rules and seeing how they interact. If they are all over the place, that is much harder. Another reason I tend to place them together is that these predicates are rarely reusable, they are tightly connected to where theyāre called from.
Of course, when you find you need begin
in lots of places and it becomes more complicated it makes sense. However, I would recommend making them parameterizable so you can control them from the trace
predicate. This keeps the āknowledgeā of the ātraceā predicate local. Now you basically have 4 places that share a huge amount of knowledge. Yes, you have the names but to understand the names I need to see the code anyway because the intention they reflect might not be true (anymore). Every model will undergo a lot of tinkering and it is easy to forget to update a name. Especially long names tend to be badly maintained in my experience as a developer.
Redundancy
I was surprised you repeated handling the unchanged part for each action predicate. Imho any redundancy is going to bite. I would definitely handle that outside the actions. Any reasoning for that?
Conclusion
Again these are personal opinions so feel free to ignore. There is no best way and Iād love to see how others are addressing these issues.