Database Operations¶
Clausal Prolog supports runtime modification of the predicate database — adding and removing clauses while a program is running. This enables dynamic state, memoization, and self-modifying programs.
Quick Example¶
-dynamic(color/1)
color('red'),
color('blue'),
test("assert and query") <- (
assertz(color('green')),
color('green')
) # nv
Declare first: the -dynamic directive¶
assertz, asserta and retract work only on a predicate declared
-dynamic. Every other predicate is static once its module has loaded, as in
ISO Prolog: modifying it raises
permission_error(modify, static_procedure, Name/Arity), with the builtin
as the culprit (Scryer's form; see Exceptions).
-dynamic(seen/1)
-private([error(formal, context), permission_error(action, type, culprit),
modify, static_procedure])
stable(1),
test("a dynamic predicate accepts assertz") <- (
assertz(seen('x')),
seen('x')
)
test("a static predicate refuses it") <- (
catch(assertz(stable(2)), E, True),
E is error(permission_error(modify, static_procedure, '/'('stable', 1)), '/'('assertz', 1))
)
The declaration also makes the predicate exist before it has clauses: a
-dynamic predicate with no clauses simply fails, where an undeclared name is
refused with the same permission_error(modify, static_procedure, Name/Arity)
(it is static by default). The module flag
assert_creates_dynamic changes that last
case to ISO 7.5.2(2): with it true, asserting into a procedure that does not
exist creates it as a dynamic procedure. An imported .pl module has it on.
Declare several at once:
See Directives for the other directives.
From Python: Module.declare_dynamic¶
A Module built from Python has no directives, so declare its dynamic
predicates with Module.declare_dynamic(name, arity), the same declaration
as -dynamic(name/arity). It also works on the Module of a loaded
.seam or .clausal file (mod.__clausal_module__).
from clausal import Module, Var, solve
m = Module("counter")
m.declare_dynamic("count", 1) # -dynamic(count/1)
list(solve(("assertz", ("count", 0)), module=m))
n = Var()
[n.value for _ in solve(("count", n), module=m)] # [0]
It is idempotent. Its errors are ISO dynamic/1's, raised as a
LogicException whose culprit is dynamic/1: instantiation_error for an
unbound argument, type_error(atom, Name), type_error(integer, Arity),
domain_error(not_less_than_zero, Arity), and
permission_error(modify, static_procedure, Name/Arity) for a builtin or a
static predicate that already has clauses.
Adding Clauses¶
assertz/1¶
assertz(Term) — add a fact at the end of the clause list (like Prolog's
assertz).
asserta/1¶
asserta(Term) — add a fact at the beginning of the clause list (like
Prolog's asserta). The new clause will be tried first on subsequent queries.
-dynamic(priority/1)
priority('low'),
test("assert first") <- (
asserta(priority('high')),
priority('high')
) # nv
Removing Clauses¶
retract/1¶
retract(Term) — remove the first clause whose head unifies with Term.
-dynamic(item/1)
item('a'),
item('b'),
item('c'),
test("retract") <- (
retract(item('b')),
(not item('b')),
item('a'),
item('c')
) # nv
As in ISO (8.9.3) and Scryer, retract of a name nothing declares simply
fails — there is no clause to remove — while a static predicate, a declared
data functor or a builtin raises
error(permission_error(modify, static_procedure, Name/Arity), retract/1), and
an unbound Term raises instantiation_error. (retractall/1 and
abolish/1 are not provided.)
retract uses unification for matching, so you can retract by pattern:
-dynamic(pair/2)
pair('x', 1),
pair('y', 2),
pair('z', 3),
test("retract by pattern") <- (
retract(pair('y', _)),
(not pair('y', 2))
) # nv
Table Management¶
abolish_table/2¶
abolish_table(functor, Arity) — clear cached answers for a specific tabled
predicate.
-table(memo_fib/2)
memo_fib(0, 0),
memo_fib(1, 1),
test("clear") <- abolish_table('memo_fib', 2) # nv
abolish_all_tables/0¶
abolish_all_tables() — clear all tabling caches at once.
Patterns & Recipes¶
Memoization¶
Cache computed results in a dynamic predicate. Arithmetic goes through a
constraint (N1 == N - 1): a bare X is N - 1 is unification and would
bind X to the unevaluated term (see Operators).
-dynamic(fib_cache/2)
fib(N, F) <- fib_cache(N, F)
fib(N, F) <- (
not fib_cache(N, _),
fib_compute(N, F),
assertz(fib_cache(N, F))
)
fib_compute(0, 0),
fib_compute(1, 1),
fib_compute(N, F) <- (
N > 1,
N1 == N - 1,
N2 == N - 2,
fib(N1, F1),
fib(N2, F2),
F == F1 + F2
)
test("memoized fib") <- (fib(30, F), F == 832040, fib_cache(30, 832040))
(For automatic memoization, consider -table instead.)
Counter / mutable state¶
-dynamic(ctr/1)
ctr(0),
increment(NEW) <- (
retract(ctr(OLD)),
NEW == OLD + 1,
assertz(ctr(NEW))
)
test("counter") <- (
increment(1),
increment(2),
ctr(2)
)
Collecting facts from a computation¶
-dynamic(result/1)
collect_evens(LIST) <- (
in_(X, LIST),
X % 2 == 0,
assertz(result(X)),
False
)
collect_evens(_),
test("collect") <- (collect_evens([1, 2, 3, 4]), findall(X, result(X), [2, 4]))
(Prefer findall for this pattern — it is cleaner and
does not require dynamic predicates.)
Gotchas¶
- Must declare
-dynamic— without it, assertz/asserta/retract raiseerror(permission_error(modify, static_procedure, Name/Arity), assertz/1)(catchable bycatch/3). A name with no declaration and no clauses is refused too by assertz/asserta, unless the module sets the flagassert_creates_dynamic(an imported.plmodule does); retract of such a name just fails. To match the culprit in a catch pattern, quote it:'/'('assertz', 1)(a bare builtin name in a term is the builtin's object, not the atom). - assertz adds facts, not rules —
assertz(foo(X) <- bar(X))is not supported and raisespermission_error(assert, rule, Head)at assert time (the predicate's existing clauses are left untouched). Only ground or partially-ground facts can be asserted. - Arity is part of the name —
assertz(foo(a))when onlyfoo/2is declared dynamic is refused witherror(permission_error(modify, static_procedure, foo/1), assertz/1):foo/1is a different predicate, it is not declared, and an undeclared procedure is static (ISO 7.5.2). - retract removes one clause — it removes the first matching clause only.
Call it in a loop (or use
findall+ multiple retracts) to remove all matches. - Order matters —
assertzappends,assertaprepends. The clause order affects which solution is found first. - Tabling interaction — if a tabled predicate depends on dynamic facts,
remember to
abolish_tableafter modifying the facts, or the cached answers will be stale.
See also: Directives — -dynamic and other predicate
directives, Tabling — automatic memoization with -table,
Meta-Predicates — findall as an alternative to
assert-based collection.