Hey Hillel,
I've been burned too many times by trying to write variables as records of multiple things that could rightfully be variables (like you showed in the first module). The main issue is that you lose the ability to compose actions that only change a few variables each when the actions try to change different fields of the same variable. E.g.
---- MODULE ex ----
EXTENDS Naturals
VARIABLE x
Init == x = [ "fst" |-> 0, "snd" |-> 0 ]
TypeOK == x \in [ "fst" : Nat, "snd" : Nat ]
Incr_fst == x' = [ x EXCEPT !.fst = @ + 1 ]
Incr_snd == x' = [ x EXCEPT !.snd = @ + 1 ]
\* Unfortunately, this doesn't make sense here as both (sub)actions attempt to set the successor value of x... bummer :(
Incr_both ==
/\ Incr_fst
/\ Incr_snd
\* ...
Next ==
\/ Incr_both
\/ ...
\* ...
====
So I'll always choose to have more variables + composability of (sub)actions + control over less variables.
To manage the amount of names I have to write following UNCHANGED, I usually write several inclusive and exclusive variable tuples; e.g. say we have the variables x1, x2, ..., xN and I have a few actions which change only x1 and x2 and leave x3, ..., xN unchanged (this would suggest an exclusive variable tuple to me). I'll define a tuple, call it non_x1_x2_vars == <<x3, ..., xN>>, and then just write UNCHANGED non_x1_x2_vars in the aforementioned actions. As long as you write all your actions in one module, this seems to work really well. Not to mention when you have 10+ variables in a spec, it becomes next to impossible to visually inspect whether you have included all the unchanged variables.
I first saw this little trick in the
raft spec where they use several (what I would call) inclusive variable tuples.
Best,