Skip to content

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:

-dynamic(counter/1)
-dynamic(cache/2)

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).

-dynamic(fact/1)

fact('init'),

test("assert") <- (
    assertz(fact(42)),
    fact(42)
)  # nv

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.

test("clear all") <- abolish_all_tables()

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 raise error(permission_error(modify, static_procedure, Name/Arity), assertz/1) (catchable by catch/3). A name with no declaration and no clauses is refused too by assertz/asserta, unless the module sets the flag assert_creates_dynamic (an imported .pl module 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 raises permission_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 only foo/2 is declared dynamic is refused with error(permission_error(modify, static_procedure, foo/1), assertz/1): foo/1 is 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 — assertz appends, asserta prepends. The clause order affects which solution is found first.
  • Tabling interaction — if a tabled predicate depends on dynamic facts, remember to abolish_table after 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.