Unification and variables
A unification variable stands for a term not yet known; it is assigned at most once, and backtracking undoes the assignment (Logic Programming, Constraints). The flags and builtins around that mechanism follow: inspecting a variable, the pattern fragment, the occur check, and the wildcard.
Unification variable inspection
A rule head does not normally fire on an unassigned variable: an input
argument is matched, and matching against a rigid pattern would have to
assign the hole (Constraints). To write a rule that does
fire on a hole, match it in the head with the uvar keyword. It comes in a
few shapes:
uvarmatches any hole;uvar as Xmatches a hole and bindsXto the whole term, for the rest of the rule to use:even (uvar as X) :- !, declare_constraint (even X) [X].
uvar Hd Argsmatches an applied hole, bindingHdto the bare variable andArgsto the list of arguments it is applied to (the names in scope where it was created, followed by any it is explicitly applied to);uvar Hd Args as Xis the same, and bindsXto the whole applied term.
../code/uvar-pattern.elpi:
1% `uvar` as a head pattern fires a rule on an argument that is still an
2% unassigned unification variable:
3% uvar — any hole
4% uvar as X — ... and bind X to the whole term
5% uvar Hd Args — an *applied* hole: Hd is the bare variable,
6% Args the list of arguments it is applied to
7% uvar Hd Args as X — ... and bind X to the whole applied term
8
9pred classify any.
10classify (uvar _ [] as X) :- !, print "unapplied hole:" X.
11classify (uvar Hd Args as X) :- !, print "applied hole:" X "-- head" Hd "args" Args.
12classify T :- print "not a hole:" T.
13
14main :-
15 classify Hole_,
16 (pi a\ pi b\ classify (Applied_ a b)),
17 classify 42.
unapplied hole: X0
applied hole: X1 c0 c1 -- head X1 args [c0, c1]
not a hole: 42
The builtin var is the same decomposition from a rule body, on a term the
head did not already take apart with uvar: var V checks that V is
unassigned, and var V Hd Args splits an applied one into its head Hd
and its argument list Args.
The pattern fragment
Elpi’s unification is decidable and well-behaved as long as it stays within
the pattern fragment (Lλ, Miller’s higher-order patterns,
Miller, 1991): a unification variable applied only to
distinct bound names. Outside of it, unification can have several, or
infinitely many, most general solutions. Elpi does not guess: by default it
aborts with an error on such a problem. The deprecated
-delay-problems-outside-pattern-fragment flag makes it suspend the problem
as a constraint instead, the way Teyjus does (Compatibility with Teyjus, Prolog and legacy Elpi).
distinct_names L checks that L is a list of pairwise-distinct bound
names, so, given the Args from var above, it tells whether a
variable is currently in the pattern fragment:
pred in-pattern-fragment any.
in-pattern-fragment X :- var X _ Args, distinct_names Args, !.
in-pattern-fragment _ :- print "outside the pattern fragment".
A variable’s own arguments, the names in scope when it was created via
pi/sigma, are always distinct, so a freshly allocated variable is
always in the fragment; it is only later applications, unification, or
explicit reuse across scopes that can push a term outside it.
The occur check
Unification performs the occur check by default: assigning a variable to a term that already contains it is rejected, rather than building a cyclic term. Most Prolog systems default the other way, and Elpi’s choice is one of the deliberate departures collected in Compatibility with Teyjus, Prolog and legacy Elpi.
ground_term T checks that T holds no unification variables at all,
useful for asserting that a computation has fully finished, for instance
before serializing a term.
The occur check can be turned off for one predicate with the :nooc
signature attribute. The standard library’s unsound_unif is plain
unification with the check removed, and is defined with :nooc and
nothing else:
% Unification without occur check. It can create infinite terms.
:nooc
func unsound_unif -> A, A.
unsound_unif X X.
So X = f X fails the occur check, but unsound_unif Y (f Y) succeeds,
tying Y into a cyclic term:
../code/nooc.elpi:
1% Unification (=) performs the occur check by default; a predicate marked
2% :nooc, like the builtin unsound_unif, does not.
3
4data tm.
5symb f tm -> tm.
6
7main :-
8 ( X = f X, print "should not happen"
9 ; print "= rejects X = f X (occur check)" ),
10 ( unsound_unif Y (f Y), print "unsound_unif Y (f Y) succeeds"
11 ; print "should not happen" ).
= rejects X = f X (occur check)
unsound_unif Y (f Y) succeeds
Code that uses :nooc then has to keep from ever building such a term:
ground_term does not terminate on a cyclic term, and neither do most
other term traversals.
The wildcard _
_ is a true wildcard, not a variable: every occurrence is independent, so
two _ in the same head are never forced to be equal, unlike a repeated
named variable:
../code/wildcard.elpi:
1% A repeated named variable forces its occurrences to be equal; `_` never
2% does, even used more than once in the same head.
3
4pred same int, int.
5same X X. % the repeated X forces the two arguments to be equal
6
7pred any int, int.
8any _ _. % each _ is an independent wildcard: never forces anything
9
10main :-
11 (same 1 2, print "same 1 2 succeeds (unexpected)" ; print "same 1 2 fails, as expected"),
12 (any 1 2, print "any 1 2 succeeds, as expected" ; print "any 1 2 fails").
same 1 2 fails, as expected
any 1 2 succeeds, as expected