Skip to content

Use Python objects from Prolog

OOPLib (tuprolog.solve.libs.OOPLib) lets Prolog code create Python objects, call their methods, and read or assign their attributes. This guide shows the common tasks; the OOPLib reference lists all its predicates and operators.

Load the library

Pass OOPLib to the solver (combine it with other libraries via tuprolog.solve.library.libraries):

>>> from tuprolog.solve.libs import OOPLib
>>> from tuprolog.solve.prolog import prolog_solver
>>> solver = prolog_solver(libraries=OOPLib)

Call functions and methods

Result := Receiver.method(Args) calls a method and binds its result. $name denotes a well-known object, such as a safe standard module ($math, $json, $re, ...), a built-in type ($list, $dict, $str, ...), or $builtins:

>>> list(solver.query("X := $math.sqrt(16)"))
[{'X': 4.0}]
>>> list(solver.query("X := $builtins.len([a, b, c])"))
[{'X': 3}]
>>> list(solver.query("X := hello.upper"))  # any term is a receiver; no-argument calls need no ()
[{'X': 'HELLO'}]
>>> list(solver.query("X := $json.dumps([1, 2], indent = 0)"))  # keyword arguments as Key = Value
[{'X': '[\n1,\n2\n]'}]

Arguments are converted from terms into Python values (atoms become strings, lists lists, ...). Results that are numbers, booleans or strings become terms.

Create objects and keep references

Other results become references: atoms denoting the objects, which later calls accept in their place. new_object(Type, Args, Object) creates an object; Receiver.method(Args) alone calls a method, discarding its result:

>>> goal = "new_object($list, [[c, a, b]], L), L.sort, L.append(d), list_items(L, Items)"
>>> list(solver.query(goal))
[{'L': '<object:list#...>', 'Items': ['a', 'b', 'c', 'd']}]
>>> list(solver.query("new_object($dict, [], D), D.update(a = 1), X := D.get(a)"))
[{'D': '<object:dict#...>', 'X': 1}]

list_items/2, set_items/2 and array_items/2 turn collections into Prolog lists.

Read and assign attributes

>>> list(solver.query("type('fractions.Fraction', F), new_object(F, [1, 3], X), D := X.denominator"))
[{'F': '<type:fractions.Fraction>', 'X': '<object:Fraction#...>', 'D': 3}]

Receiver.attribute := Value assigns an attribute (see the next section for an example).

Convert values

Result := (Expression) as Type converts a value by calling a type:

>>> list(solver.query("X := ('42') as $int"))
[{'X': 42}]

Expose your own objects

oop_lib(aliases={...}) builds an OOPLib instance where your objects and types have aliases:

>>> from tuprolog.solve.libs import oop_lib
>>> class Point:
...     def __init__(self, x, y):
...         self.x, self.y = x, y
...
...     def norm(self):
...         return (self.x**2 + self.y**2) ** 0.5
>>> solver = prolog_solver(libraries=oop_lib(aliases={"point": Point}))
>>> list(solver.query("new_object($point, [3, 4], P), N := P.norm, P.x := 6, X := P.x"))
[{'P': '<object:Point#...>', 'N': 5.0, 'X': 6}]

Prolog code can also register aliases with register(Object, Alias):

>>> list(solver.query("new_object($point, [1, 1], P), register(P, origin), X := $origin.x"))
[{'P': '<object:Point#...>', 'X': 1}]

Lift the safety limits

By default only side-effect free standard modules can be imported (by type/2 or as aliases), built-ins giving access to code execution or files (eval, open, ...) are unavailable, and private attributes (_name) are not accessible:

>>> solver = prolog_solver(libraries=OOPLib)
>>> solver.solve_once("type('os', T)").exception.type
existence_error(oop_alias, os)
>>> solver.solve_once("X := hello.'__len__'").exception.type
permission_error(access, oop_method, '__len__')

oop_lib(allow_all_imports=True, allow_private=True) lifts these limits:

>>> solver = prolog_solver(libraries=oop_lib(allow_all_imports=True, allow_private=True))
>>> list(solver.query("type('os.path', P), X := P.basename('/tmp/kb.pl')"))
[{'P': '<module:...path>', 'X': 'kb.pl'}]
>>> list(solver.query("X := hello.'__len__'"))
[{'X': 5}]

Warning

These limits restrict what Prolog code can reach, but they are not a sandbox: do not run untrusted Prolog code with OOPLib.

See also