Clauses may use ‘state variables’ as a shorthand for naming intermediate values in a sequence. That is, where in the plain syntax one might write
main(IO0, IO) :- io.write_string("The answer is ", IO0, IO1), io.write_int(calculate_answer(…), IO1, IO2), io.nl(IO3, IO).
using state variable syntax one could write
main(!IO) :- io.write_string("The answer is ", !IO), io.write_int(calculate_answer(…), !IO), io.nl(!IO).
A state variable is written ‘!.X’ or ‘!:X’, denoting the “current” or “next” value of the sequence labelled X. An argument ‘!X’ is shorthand for two state variable arguments ‘!.X, !:X’; that is, ‘p(…, !X, …)’ is parsed as ‘p(…, !.X, !:X, …)’.
State variables obey special scope rules. A state variable X must be explicitly introduced, and this can happen in one of four ways:
A state variable X in the enclosing scope of a lambda or if-then-else expression may only be referred to as ‘!.X’ (unless the enclosing X is shadowed by a more local state variable of the same name.)
For instance, the following clause employing a lambda expression
p(A, B, !S) :- F = ( pred(C::in, D::out) is det :- q(C, D, !S) ), ( if F(A, E) then B = E else B = A ).
is illegal because it implicitly refers to ‘!:S’ inside the lambda expression. However
p(A, B, !S) :- F = ( pred(C::in, D::out, !.S::in, !:S::out) is det :- q(C, D, !S) ), ( if F(A, E, !S) then B = E else B = A ).
is acceptable because the state variable S accessed inside the lambda expression is locally scoped to the lambda expression (shadowing the state variable of the same name outside the lambda expression), and the lambda expression may refer to the next version of a local state variable.
There are two restrictions concerning state variables in functions, whether they are defined by clauses or lambda expressions.
Within each clause, the compiler replaces each occurrence of !X in an argument list with two arguments: !.X, !:X, where !.X represents the current version of the state of !X, and !:X represents its next state. It then replaces all occurrences of !.X and !:X with ordinary variables in way that (in the general case) represents a sequence of updates to the state of X from an initial state to a final state.
This replacement is done by code that is equivalent to the ‘transform_goal’ and ‘transform_clause’ functions below. The basic operation used by these functions is substitution: ‘substitute(Goal, [!.X -> CurX, !:X -> NextX])’ stands for a copy of Goal in which every free occurrence of ‘!.X’ is replaced with CurX, and every free occurrence of ‘!:X’ is replaced with NextX. (A free occurrence is one not bound by the head of a clause or lambda, or by an explicit quantification.)
The ‘transform_goal(Goal, X, CurX, NextX)’ function’s inputs are
It returns a transformed version of Goal.
‘transform_goal’ has a case for each kind of Mercury goal. These cases are as follows.
Calls
Given a first order call such as ‘predname(ArgTerm1, ..., ArgTermN)’ or a higher-order call such as ‘PredVar(ArgTerm1, ..., ArgTermN)’, if any of the ArgTermI is !X, ‘transform_goal’ replaces that argument with two arguments: !.X and !:X. It then checks whether ‘!:X’ appears in the updated Call.
substitute(Call, [!.X -> CurX, !:X -> NextX])
substitute(Call, [!.X -> CurX]), NextX = CurX
Note that !.X can occur in Call on its own (i.e. without !:X). Likewise, !:X can occur in Call without !.X, but this does not need separate handling.
Note that PredVar in a higher-order call may not be a reference to a state var, i.e it may not be !X, !.X or !:X.
Unifications
In a unification ‘TermA = TermB’, each of TermA and TermB may have one of the following four forms:
Note that TermA and TermB may not have the form !S.
State variable field updates
A state variable field update goal has the form
!S ^ field_list := Term
where field_list is a valid field list See Record syntax. This means that
!S ^ field1 := Term !S ^ field1 ^ field2 := Term !S ^ field1 ^ field2 ^ field3 := Term
are all valid field update goals. If S is X, ‘transform_goal’ replaces such goals with
NextX = CurX ^ field_list := Term
Otherwise, it leaves the goal unchanged.
Conjunctions
Given a nonempty conjunction, whether a sequential conjunction such as Goal1, Goal2 or a parallel conjunction such as Goal1 & Goal2, ‘transform_goal’
substitute(Goal1, [!.X -> CurX, !:X -> MidX])
substitute(Goal2, [!.X -> MidX, !:X -> NextX])
This implies that first Goal1 updates the state of X from CurX to MidX, and then Goal2 updates the state of X from MidX to NextX.
Given the empty conjunction, i.e. the goal ‘true’, ‘transform_goal’ will replace it with
NextX = CurX
Disjunctions
Given a disjunction such as Goal1 ; Goal2, ‘transform_goal’
substitute(Goal1, [!.X -> CurX, !:X -> NextX])
substitute(Goal2, [!.X -> CurX, !:X -> NextX])
This shows that both disjuncts start with the CurX, and both end with NextX. If a disjunct has no update of !X, then the value of NextX in that disjunct will be set to CurX.
The empty disjunction, i.e. the goal ‘fail’, cannot succeed, so what the value of !:X would be if it did succeed is moot. Therefore ‘transform_goal’ returns empty disjunctions unchanged.
Negations
Given a negated goal of the form ‘not NegatedGoal’ ‘transform_goal’
‘not’ substitute(‘NegatedGoal’, [!.X -> CurX, !:X -> DummyX]), NextX = CurX
It does this because negated goals may not generate any outputs visible from the rest of the code, which means that any output they do generate must be local to the negated goal.
Negations that use ‘\+ NegatedGoal’ notation are handled exactly the same way,
If-then-elses
Given an if-then-else, whether it uses ( if Cond then Then else Else ) syntax or ( Cond -> Then ; Else ) syntax, ‘transform_goal’
substitute(Cond, [!.X -> CurX, !:X -> MidX])
substitute(Then, [!.X -> MidX, !:X -> NextX])
substitute(Else, [!.X -> CurX, !:X -> NextX])
This effectively treats an if-then-else as being a disjunction, with the first disjunct being the conjunction of the Cond and Then goals, and the second disjunct being the Else goal. (The Else goal is implicitly conjoined inside the second disjunct with the negation of the existential closure of Cond, since the else case is executed only if the condition has no solution.)
Bidirectional implications
‘transform_goal’ treats a bidirectional implication goal, which has the form GoalA <=> GoalB, as it were the conjunction of its two constituent unidirectional implications: GoalA => GoalB, GoalA <= GoalB.
Unidirectional implications
‘transform_goal’ treats a unidirectional implication, which has one of the two forms ‘GoalA => GoalB’ and ‘GoalB <= GoalA’, as if they were written as ‘not (GoalA, not GoalB)’.
Universal quantifications
‘transform_goal’ treats universal quantifications, which have the form ‘all Vars SubGoal’ as if they were written as ‘not (some Vars (not SubGoal))’. Note that in universal quantifications, Vars must a list of ordinary variables.
Existential quantifications
In existential quantifications, which have the form ‘some Vars SubGoal’, Vars must be a list, in which every element must be either ordinary variable (such as A), or a state variable (such as !B). (Note that Vars may not contain any element whose form is !.B or !:B.)
substitute(SubGoal, [!.X -> CurX, !:X -> NextX])
Note that state variables in Vars are handled by ‘transform_clause’ below.
The ‘transform_clause’ function’s input is a clause, which may be a non-DCG clause or a DCG clause, which have the forms
predname(ArgTerm1, ..., ArgTermN) :- BodyGoal.
and
predname(ArgTerm1, ..., ArgTermN) --> BodyGoal.
respectively. ‘transform_clause’ handles both the same way.
Actual application of this transformation would, in the general case, result in the generation of many different versions of each state variable, for which we need more names than just ‘CurX’, ‘TmpX’ and ‘NextX’. The Mercury compiler therefore uses
This transformation can lead to the introduction of chains of unifications for variables that do not otherwise play a role in the definition, such as ‘STATE_VARIABLE_X_5 = STATE_VARIABLE_X_6, STATE_VARIABLE_X_6 = STATE_VARIABLE_X_7, STATE_VARIABLE_X_7 = STATE_VARIABLE_X_8’. Where possible, the compiler automatically shortcircuits such sequences by removing any unneeded intermediate variables. In the above case, this would yield ‘STATE_VARIABLE_X_5 = STATE_VARIABLE_X_8’.
The following code fragments illustrate some appropriate uses of state variable syntax.
main(!IO) :- io.write_string("The 100th prime is ", !IO), X = prime(100), io.write_int(X, !IO), io.nl(!IO).
foldl2(_, [], !A, !B). foldl2(P, [X | Xs], !A, !B) :- P(X, !A, !B), foldl2(P, Xs, !A, !B).
iterate_while2(Test, Update, !A, !B) :- ( if Test(!.A, !.B) then Update(!A, !B), iterate_while2(Test, Update, !A, !B) else true ).
compute_out(InA, InB, InsC, Out) :- some [!State] ( init_state(!:State), update_state_a(InA, !State), update_state_b(InB, !State), list.foldl(update_state_c, InC, !State), compute_output(!.State, Out) ).