I am implementing a unification algorithm in JavaScript to compute the most general unifier of two given terms. In brief, unification is the process of taking two terms t1 and t2 and combining them into a new term t which is specialization of both t1 and t2. Consider (note that variables are capitalized):
t1 := foo(jane, A, B)
t2 := foo(X, Y, rocks)
The specialization operator ⊏ denotes that “a is a specialization of b” in a ⊏ b. For example:
foo(jane, doe, X) ⊏ t1
foo(on, the, rocks) ⊏ t2
A unifier of two terms is a term that specializes both those terms. For example:
foo(jane, street, rocks) ⊏ t1
foo(jane, street, rocks) ⊏ t2
The most general unifier of two terms is a unifier of those two terms which generalizes all other unifiers of those two terms (i.e. all the other unifiers specialize the most general unifier). For example:
foo(jane, street, rocks) ⊏ foo(jane, M, rocks)
Hence foo(jane, M, rocks) is the most general unifier of t1 and t2. The following algorithm can be used to compute the most general unifier for terms of the first-order predicate logic.
- If both
t1andt2are constants then they unify if and only if they are the same constant (e.g.jane,streetandrocksare constants). - If
t1is a variable andt2is a non-variable (i.e. a constant or a complex term) thent1is instantiated tot2if and only ift1doesn't occur int2. - If
t2is a variable andt1is a non-variable (i.e. a constant or a complex term) thent2is instantiated tot1if and only ift2doesn't occur int1. - If
t1andt2are both variables then they are both instantiated to each other and are said to share values. - If both
t1andt2are complex terms then they unify if and only if they are the same kind of complex term and their corresponding arguments unify. - Two terms unify if and only if they unify following the above five rules.
Anyway, this is how I wanted to tackle this problem:
function variable(name) {
return {
type: "variable",
name: name,
term: null
};
}
function constant(name) {
return {
type: "non_variable",
name: name,
args: []
};
}
function complex(name, args) {
return {
type: "non_variable",
name: name,
args: args
};
}
Now, we can define t1 and t2 as follows:
var t1 = complex("foo", [constant("jane"), variable("A"), variable("B")]);
var t2 = complex("foo", [variable("X"), variable("Y"), constant("rocks")]);
Then we implement the unification algorithm:
function unify(a, b) {
var x = resolve(a);
var y = resolve(b);
var operation = 0;
if (x.type === "non_variable") operation += 2;
if (y.type === "non_variable") operation += 1;
switch (operation) {
case 0:
return x.term = y.term = variable(x.name);
case 1:
if (occurs(x, y)) throw new Error(x.name + " occurs in " + show(y));
return x.term = y;
case 2:
if (occurs(y, x)) throw new Error(y.name + " occurs in " + show(x));
return y.term = x;
case 3:
if (x.name !== y.name) throw new
Error(x.name + " and " + y.name + " are different terms");
var ax = x.args;
var ay = y.args;
if (ax.length !== ay.length) throw new
Error(x.name + " and " + y.name + " have different arities");
var args = ax.map(function (t, i) {
return unify(t, ay[i]);
});
return complex(x.name, args);
}
}
function resolve(t) {
if (t.type === "non_variable") return t;
var v = t;
while (v.type === "variable" && v.term) v = v.term;
return t.term = v;
}
function show(t) {
return t.name + "(" + t.args.map(function (t) {
return t.type === "non_variable" &&
t.args.length > 0 ? show(t) : t.name;
}).join(", ") + ")";
}
function occurs(v, t) {
return t.args.some(function (t) {
t = resolve(t);
return t.type === "variable" ? t === v : occurs(v, t);
});
}
And it works:
var t = unify(t1, t2);
alert(show(t));
<script>
function variable(name) {
return {
type: "variable",
name: name,
term: null
};
}
function constant(name) {
return {
type: "non_variable",
name: name,
args: []
};
}
function complex(name, args) {
return {
type: "non_variable",
name: name,
args: args
};
}
var t1 = complex("foo", [constant("jane"), variable("A"), variable("B")]);
var t2 = complex("foo", [variable("X"), variable("Y"), constant("rocks")]);
function unify(a, b) {
var x = resolve(a);
var y = resolve(b);
var operation = 0;
if (x.type === "non_variable") operation += 2;
if (y.type === "non_variable") operation += 1;
switch (operation) {
case 0:
return x.term = y.term = variable(x.name);
case 1:
if (occurs(x, y)) throw new Error(x.name + " occurs in " + show(y));
return x.term = y;
case 2:
if (occurs(y, x)) throw new Error(y.name + " occurs in " + show(x));
return y.term = x;
case 3:
if (x.name !== y.name) throw new
Error(x.name + " and " + y.name + " are different terms");
var ax = x.args;
var ay = y.args;
if (ax.length !== ay.length) throw new
Error(x.name + " and " + y.name + " have different arities");
var args = ax.map(function (t, i) {
return unify(t, ay[i]);
});
return complex(x.name, args);
}
}
function resolve(t) {
if (t.type === "non_variable") return t;
var v = t;
while (v.type === "variable" && v.term) v = v.term;
return t.term = v;
}
function show(t) {
return t.name + "(" + t.args.map(function (t) {
return t.type === "non_variable" &&
t.args.length > 0 ? show(t) : t.name;
}).join(", ") + ")";
}
function occurs(v, t) {
return t.args.some(function (t) {
t = resolve(t);
return t.type === "variable" ? t === v : occurs(v, t);
});
}
</script>
However, I have only one problem: the way I have implemented first-class variables is not very nice. The way I have implemented it is as follows:
- Every variable has a
termproperty which isnullinitially (to show that the variable has not yet been instantiated). - When a variable is unified with a non-variable its
termproperty is set to the non-variable, which is fine. - However, when a variable is unified with another variable then a problem arises. We can't set the
termproperty of one variable to the other because that would result in a cyclic reference. Therefore, I create a new variable and set bothtermproperties of both the variables to the new variable. - This causes another problem. If we unify lots of variables then the
termchain will become longer which is why I need theresolvefunction in my code above. It traverses thetermchain and returns the last term in the chain.
So my question is whether there is a better way to create first-class variables in JavaScript (i.e. one in which sharing values between two variables doesn't require the use of a term chain). If there is a better way then please describe the algorithm.
It sounds like you could use a disjoint-set-forest that the terms would be members of. This has efficient union/find.
http://en.wikipedia.org/wiki/Disjoint-set_data_structure#Disjoint-set_forests
A bit of googling reaffirms that this is used in prolog unification implementations.
http://docs.lib.purdue.edu/cgi/viewcontent.cgi?article=2318&context=cstech