Skip to content

Define predicates in Python

Solvers can call Python code: a predicate (e.g. upper(abc, U)) or an arithmetic function (e.g. X is half(3)) can be implemented by a Python function. Implementations are packed into libraries, which are given to solvers. This guide goes from the simplest way to the most flexible one.

Write a predicate as a plain function

tuprolog.solve.primitive.predicate turns a Python function into a predicate. The goal's arguments are converted into Python values (atoms become strings, numbers numbers, lists lists; unbound variables stay tuprolog.core.Vars), and the function either:

  • returns a boolean: whether the goal succeeds (without binding variables);
  • returns, or yields, tuples of argument values, one per solution: each tuple is unified with the goal's arguments, binding its variables (tuples that do not unify are skipped). For predicates of arity 1, plain values work too.
>>> from tuprolog.solve.library import library
>>> from tuprolog.solve.primitive import predicate
>>> from tuprolog.solve.prolog import prolog_solver
>>> def between(low, high, _):
...     return ((low, high, x) for x in range(low, high + 1))
>>> lib = library(
...     "my.lib",
...     primitives={
...         "even/1": predicate(lambda n: n % 2 == 0),
...         "upper/2": predicate(lambda text, _: [(text, text.upper())]),
...         "between/3": predicate(between),
...     },
... )
>>> solver = prolog_solver(libraries=lib)
>>> list(solver.query("between(1, 6, X), even(X)"))
[{'X': 2}, {'X': 4}, {'X': 6}]
>>> list(solver.query("upper(abc, U)"))
[{'U': 'ABC'}]

Predicates are registered by indicator, name/arity. Yielding solutions lazily, as between does, lets solvers stop as soon as they have enough.

Write an arithmetic function

tuprolog.solve.function.function turns a Python function into an arithmetic function, usable wherever Prolog evaluates expressions (is/2, </2, ...). Its arguments are evaluated first:

>>> from tuprolog.solve.function import function
>>> lib = library("my.math", functions={"half/1": function(lambda x: x / 2)})
>>> list(prolog_solver(libraries=lib).query("X is half(3) + 1"))
[{'X': 2.5}]

Bundle predicates, functions, clauses and operators

A library can hold any mix of primitives, functions, clauses written in Prolog, and operators:

>>> from tuprolog.core.operators import DEFAULT_OPERATORS, XFX, operator, operator_set
>>> from tuprolog.theory.parsing import parse_clauses
>>> shouts = operator_set(operator("shouts", XFX, 700))
>>> lib = library(
...     "shout",
...     primitives={"upper/2": predicate(lambda text, _: [(text, text.upper())])},
...     clauses=parse_clauses(  # parsed with the operator, too
...         "X shouts Y :- upper(X, U), atom_concat(U, '!', Y).", DEFAULT_OPERATORS + shouts
...     ),
...     operators=shouts,
... )
>>> list(prolog_solver(libraries=lib).query("hello shouts Y"))
[{'Y': 'HELLO!'}]

Give several libraries to a solver with tuprolog.solve.library.libraries:

>>> from tuprolog.solve.library import libraries
>>> from tuprolog.solve.libs import OOPLib
>>> solver = prolog_solver(libraries=libraries(lib, OOPLib))
>>> sorted(solver.libraries.aliases)  # prolog.lang holds the standard predicates
['prolog.lang', 'prolog.oop.python', 'shout']

Report errors

Exceptions raised by your Python code become logic errors: a system_error, which Prolog code can catch with catch/3, and which halts the query otherwise:

>>> solver = prolog_solver(
...     libraries=library(
...         "my.lib",
...         primitives={
...             "upper/2": predicate(lambda text, _: [(text, text.upper())]),
...         },
...     )
... )
>>> solver.solve_once("catch(upper(42, U), error(E, _), true)")["E"]
system_error

To raise the standard ISO errors instead (e.g. type_error(atom, 42)), write a primitive, as shown next, and check its arguments with the ensuring_* functions.

Write a primitive

tuprolog.solve.primitive.primitive gives full control: the Python function receives the solve request (request.arguments as terms, request.context with the solver's state) and yields solve responses, one per solution, built by the request's reply_* methods:

Method Meaning
request.reply_success(substitution) (or reply_with) a solution, binding variables as the substitution says
request.reply_fail() no (more) solutions
request.reply_exception(error) an error

The ensuring_* functions check arguments, raising the matching ISO error (e.g. ensuring_argument_is_atom(request, 0) raises type_error(atom, Culprit) unless the first argument is an atom):

>>> from tuprolog.core import atom
>>> from tuprolog.solve.primitive import ensuring_argument_is_atom, primitive
>>> def upper(request):
...     ensuring_argument_is_atom(request, 0)
...     text, result = request.arguments
...     upper_case = atom(str(text.value).upper())
...     yield request.reply_with(request.context.unificator.mgu(result, upper_case))
>>> solver = prolog_solver(libraries=library("my.lib", primitives={"upper/2": primitive(upper)}))
>>> list(solver.query("upper(abc, U)"))
[{'U': 'ABC'}]
>>> solver.solve_once("catch(upper(42, U), error(E, _), true)")["E"]
type_error(atom, 42)

Primitives can also change the solver's state (e.g. add clauses, set flags) via the side effects of tuprolog.solve.sideffects, attached to responses.

See also