Implement a custom Unificator¶
How to change the way two Terms are compared during unification, and how to control occurs-check, using the
unify module's Unificator API.
This is a usage recipe; see the Explanation section for the rationale behind pluggable unification strategies.
1. Use a built-in strategy¶
Unificator's companion object ships two ready-made strategies:
term1: Term,
term2: Term,
): Boolean = match(term1, term2, true)
Unificator.default(an alias forstrict()) compares terms with plainTerm.equals.Unificator.naive()behaves likestrict(), except it compares numbers by their numeric value rather than their exact representation (so1and1.0can be considered equal).
val strict = Unificator.strict()
val naive = Unificator.naive()
val substitution = strict.mgu(term1, term2)
Both factory methods accept an optional starting context: Substitution of pre-existing bindings to unify
against:
val context: Substitution = Substitution.of(Var.of("X") to Atom.of("a"))
val strictWithContext = Unificator.strict(context)
2. Disable occurs-check for a single call¶
mgu, match, and unify all accept an occurCheckEnabled: Boolean parameter (true by default):
* that same variable (which would otherwise produce an infinite/cyclic term); disabling it trades soundness for
* speed, which is safe only when the caller already knows the operands cannot give rise to such a cycle.
*
* When the operands have distinct semantic roles, the subject term (such as a goal, query, actual value, or sought
* item) should be passed first, and the reference term (such as a pattern, rule head, expected value, or stored
* candidate) second. Calls whose operands have no such roles may retain their natural or mathematical order.
*
val unificator = Unificator.default
val substitution = unificator.mgu(term1, term2, occurCheckEnabled = false)
The infix operators mguWith, matches, and unifyWith always perform occurs-check, since they delegate to
Unificator.default; use the explicit method calls above if you need to disable it.
3. Implement a custom unification strategy¶
Subclass AbstractUnificator and override checkTermsEquality, which decides whether two terms are
considered equal while building the unification equations. For reference, this is how the built-in naive()
strategy implements value-based comparison of numbers:
): Term? = unify(term1, term2, true)
/**
* Merges [substitution1] and [substitution2] into a single [Substitution], as if their bindings had been
* collected while unifying two terms piecewise (e.g. argument by argument): equal-variable bindings from both
* sides are unified against each other and, optionally, checked for occurrence, rather than simply overwritten.
*
* @return the merged [Substitution], or [Substitution.failed] if [substitution1] and [substitution2] disagree
* on some variable's binding (or either of them, or [context], is already failed).
*/
@JsName("mergeWithOccurCheck")
fun merge(
substitution1: Substitution,
substitution2: Substitution,
occurCheckEnabled: Boolean,
): Substitution
Follow the same pattern for your own strategy — for instance, comparing atoms case-insensitively:
import it.unibo.tuprolog.core.Term
import it.unibo.tuprolog.unify.AbstractUnificator
val caseInsensitive =
object : AbstractUnificator() {
override fun checkTermsEquality(
first: Term,
second: Term,
): Boolean =
when {
first.isAtom && second.isAtom ->
first.castToAtom().value.equals(second.castToAtom().value, ignoreCase = true)
else -> first == second
}
}
caseInsensitive.match(Atom.of("Foo"), Atom.of("foo")) // true
AbstractUnificator() (no-arg) starts from an empty context; pass a Substitution to the constructor if you
need a starting context, same as the built-in factories.
4. Add caching, if needed¶
Any Unificator — built-in or custom — can be wrapped to cache its results (LRU, capacity 32 by default):
@JsName("unifyWith")
infix fun Term.unifyWith(other: Term): Term? = default.unify(this, other)
/** Merges [this] and [other], using the [default] unification strategy. */
@JvmStatic
@JsName("mergeWith")
infix fun Substitution.mergeWith(other: Substitution): Substitution = default.merge(this, other)
/**
* Creates a naive unification strategy, with the given starting [context], that checks [Term]s' equality
* through [Term.equals], except for numeric terms which are compared *by value* rather than by exact
val cached = Unificator.cached(caseInsensitive)
val smallerCache = Unificator.cached(caseInsensitive, capacity = 5)
Wrapping an already-cached Unificator again just re-wraps the original with the new capacity, instead of
double-caching.