4.14.1 Managing (dynamic) predicates
- [ISO]abolish(:PredicateIndicator)
- Removes all clauses of a predicate with functor Functor and
arity
Arity from the database. All predicate attributes (dynamic,
multifile, index, etc.) are reset to their defaults. Abolishing an
imported predicate only removes the import link; the predicate will keep
its old definition in its definition module.
According to the ISO standard, abolish/1 can only be applied to dynamic procedures. This is odd, as for dealing with dynamic procedures there is already retract/1 and retractall/1. The abolish/1 predicate was introduced in DEC-10 Prolog precisely for dealing with static procedures. In SWI-Prolog, abolish/1 works on static procedures, unless the Prolog flag iso is set to
true
.It is advised to use retractall/1 for erasing all clauses of a dynamic predicate.
- abolish(+Name, +Arity)
- Same as
abolish(Name/Arity)
. The predicate abolish/2 conforms to the Edinburgh standard, while abolish/1 is ISO compliant. - copy_predicate_clauses(:From, :To)
- Copy all clauses of predicate From to To. The
predicate
To must be dynamic or undefined. If To is
undefined, it is created as a dynamic predicate holding a copy of the
clauses of
From. If To is a dynamic predicate, the clauses of
From are added (as in assertz/1)
to the clauses of To.
To and From must have the same arity. Acts as if
defined by the program below, but at a much better performance by
avoiding decompilation and compilation.
copy_predicate_clauses(From, To) :- head(From, MF:FromHead), head(To, MT:ToHead), FromHead =.. [_|Args], ToHead =.. [_|Args], forall(clause(MF:FromHead, Body), assertz(MT:ToHead, Body)). head(From, M:Head) :- strip_module(From, M, Name/Arity), functor(Head, Name, Arity).
- redefine_system_predicate(+Head)
- This directive may be used both in module
user
and in normal modules to redefine any system predicate. If the system definition is redefined in moduleuser
, the new definition is the default definition for all sub-modules. Otherwise the redefinition is local to the module. The system definition remains in the modulesystem
.Redefining system predicate facilitates the definition of compatibility packages. Use in other contexts is discouraged.
- [ISO,nondet]retract(+Term)
- When Term is an atom or a term it is unified with the first
unifying fact or clause in the database. The fact or clause is removed
from the database. The retract/1
predicate respects the logical update view. This implies that retract/1
succeeds for all clauses that match Term when the predicate
was called. The example below illustrates that the first call
to retract/1
succeeds on
bee
on backtracking despite the fact thatbee
is already retracted.85Example by Jan Burse:- dynamic insect/1. insect(ant). insect(bee). ?- ( retract(insect(I)), writeln(I), retract(insect(bee)), fail ; true ). ant ; bee.
If multiple threads start a retract on the same predicate at the same time their notion of the entry generation is adjusted such that they do not retract the same first clause. This implies that, if multiple threads use
once(retract(Term))
, no two threads will retract the same clause. Note that on backtracking over retract/1, multiple threads may retract the same clause as both threads respect the logical update view. - [ISO,det]retractall(+Head)
- All facts or clauses in the database for which the head unifies with Head are removed. If Head refers to a predicate that is not defined, it is implicitly created as a dynamic predicate. See also dynamic/1.86The ISO standard only allows using dynamic/1 as a directive.
- [ISO]asserta(+Term)
- [ISO]assertz(+Term)
- [deprecated]assert(+Term)
- Assert a clause (fact or rule) into the database. The predicate
asserta/1
asserts the clause as first clause of the predicate while
assertz/1
assert the clause as last clause. The deprecated assert/1
is equivalent to assertz/1.
If the program space for the target module is limited (see set_module/1), asserta/1
can raise a
resource_error(program_space)
exception. The example below adds two facts and a rule. Note the double parentheses around the rule.?- assertz(parent('Bob', 'Jane')). ?- assertz(female('Jane')). ?- assertz((mother(Child, Mother) :- parent(Child, Mother), female(Mother))).
- asserta(+Term, -Reference)
- assertz(+Term, -Reference)
- [deprecated]assert(+Term, -Reference)
- Equivalent to asserta/1, assertz/1, assert/1, but in addition unifies Reference with a handle to the asserted clauses. The handle can be used to access this clause with clause/3 and erase/1.
4.14.1.1 Transactions
Traditionally, Prolog database updates add or remove individual clauses. The Logical Update View ensures that a goal that is started on a dynamic predicate does not see modifications due to assert/1 or retract/1 during its life time. See section 4.14.5. In a multi-threaded context this assumption still holds for individual predicates: concurrent modifications to a dynamic predicate are invisible.
Transactions allow running a goal in isolation. The goals running inside the transaction‘see’the database as it was when the transaction was started together with database changes done by the transaction goal. Other threads see no changes until the transaction is committed. The commit, also if it involved multiple clauses spread over multiple predicates, becomes atomically visible to other threads. Transactions have several benefits Wielemaker, 2013
- If a database update requires multiple assert/1
and/or retract/1
operations, a transaction ensure either all are executed or the database
remains unchanged. Notably unexpected exceptions or failures cannot
leave the database in an inconsistent state.
- Other threads do not see the intermediate inconsistent states when a
database update that consists of multiple assert and/or retract is
performed in a transaction. This notably avoids the need to use locks
(see with_mutex/2)
in threads that read the data. A reading thread may still need to use snapshot/1
if a goal depends on multiple calls to dynamic predicates. Unlike locks,
transaction and snapshot based synchronization allows both readers and
writers to make progress simultaneously.87Read-write
locks also provide readers and writers to make progress simultaneously,
but readers see all intermediate states rather than a consistent state.
Transactions on their own do not guarantee consistency. For example, when running the code below to update the temperature concurrently from multiple threads it is possible for the global state to have multiple temperature/1 clauses.
update_temperature(Temp) :- transaction(( retractall(temperature(_)), asserta(temperature(Temp)))).
Global consistency can be achieved by wrapping the above transaction using with_mutex/2 or by using transaction/3 with a constraint that demands a single clause for temperature/1
- Transactions allow for “what if” reasoning over the dynamic database. This is particularly useful when combined with the deductive database facilities provided by tabling (see section 7).
SWI-Prolog transactions only affect the dynamic database. Static predicates are globally visible and shared at all times. In particular, transactions do not affect loading source files and thus, source files loaded inside a transaction (e.g., due to autoloading) are immediately globally visible. This may pose problems if loading source files provide clauses for dynamic predicates.
- transaction(:Goal)
- transaction(:Goal, +Options)
- Run Goal as once/1
in a transaction. This implies that access to dynamic predicates‘sees’the
dynamic predicates at the moment the transaction is started, together
with the modifications issued by
Goal. Thus, Goal does not see changes to dynamic
predicates from other threads and other threads do not see modifications
by
Goal (isolation). If Goal succeeds, all
modifications become atomically visible to the other threads.
If Goal fails or raises an exception all local modifications
are discarded and transaction/1
fails or passes the exception.
Currently the number of database changes inside a transaction (or snapshot, see snapshot/1) is limited to 2 ** 32 -1. If this limit is exceeded a
representation_error(transaction_generations)
exception is raised.Transactions may be nested. The above mentioned limitation for the number of database changes applies to the combined number in nested transactions.
If Goal succeeds, the transaction is committed. This implies that (1) any clause that is asserted in the transaction and not retracted in the same transaction is made globally visible and (2) and clause the existed before the transaction and is retracted in the transaction becomes globally invisible. Multiple transactions may retract the same clause and be committed, i.e., committing a retract that was already performed is a no-op. All modifications become atomically visible to other threads. The transaction/3 variation allows for verifying constraints just before the commit takes place.
Clause ordering Inside a transaction clauses can be added using asserta/1 and assertz/1. If only a single transaction is active at any point in time transactions preserve the usual ordering of clauses. However, if multiple transactions manipulate the same predicate(s) concurrently (typically using transaction/3), the final order of the clauses is the order in which the transactions asserted the clauses and not the order in which the transactions are committed.
The transaction/1 variant is equivalent to
transaction(Goal,[])
. The transaction/2 variant processed the following options:- bulk(+Boolean)
- When
true
, accumulate events from changes to dynamic predicates (see prolog_listen/2) and trigger these events as part of the commit phase. This implies that if the transaction is not committed the events are never triggered. Failure to trigger the events causes the transaction to be discarded. Experimental.
- transaction(:Goal, :Constraint, +Mutex)
- Similar to transaction/1,
but allows verifying Constraint during the commit phase. This
predicate follows the steps below. Any failure or exception during this
process discards the transaction and releases
Mutex when applicable. Constraint may modify the
database. Such modifications follow the semantics that apply for Goal.
- Call
once(Goal)
- Lock Mutex
- Change the visibility to the current global state combined with the changes made by Goal
- Call
once(Constraint)
- Commit the changes
- Unlock Mutex.
This predicate is intended to execute multiple transactions with a time consuming Goal in part concurrently. For example, it can be used for a Compare And Swap (CAS) like design. We illustrate this using a simple counter in the code below. Note that the transaction fails if some other thread concurrently updated the counter. This is why we need the repeat/0 and a final !/0. The CAS-style update is in general useful if Goal is expensive and conflicts are rare.
:- dynamic counter/1. increment_counter(Delta) :- repeat, transaction(( counter(Value), Value2 is Value+Delta, ), ( retract(counter(Value)), asserta(counter(Value2)) ), counter_lock), !.
- Call
- snapshot(:Goal)
- Similar to transaction/1, but always discards the local modifications. In other words, snapshot/1 allows a thread to examine a frozen state of the dynamic predicates and/or make isolated modifications without affecting other threads and without making permanent changes to the database. Where transactions allow the global state to be updated atomically from one consistent state to the next, a snapshot allows reasoning about a consistent state.
- [nondet]current_transaction(-Goal)
- True when called inside a transaction running Goal. This predicate generates candidates from the current (nested) transaction outward. Goal is a plain goal if the calling context module is the same as matching transaction/1 or snapshot/1 and a qualified callable term otherwise. Note that this only enumerates transactions in the current thread.
- transaction_updates(-Updates)
- Unify Updates with a list of database updates that would be
effectuated if the transaction is going to be committed at this stage.
Updates is a list of terms defined below. The elements are
sorted on the change generation, i.e., the order in which the operations
were performed.
- asserta(+ClauseRef)
- assertz(+ClauseRef)
- The given clause will be asserted at the start or end. Note that due to competing transactions the clause may no longer be the first/last clause of the predicate.
- erased(+ClauseRef)
- The given clause will be removed. This may be due to erase/1, retract/1 or retractall/1.
4.14.1.2 Impact of transactions
Transactions interact with other facilities that depend on changing dynamic predicates. This section discusses these interactions.
- Last modified generation
- Using the predicate_property/2
property
last_modified_generation(Generation)
we can determine whether a predicate was modified. When a predicate is changed inside a transaction this generation is not updated. The generation for dynamic predicates that are modified in the transaction is updated to the commit generation when the transaction is committed. Asking for the last modified generation inside the transaction examines the log of modified clauses and reports the generation as one of- The global modified generation if the predicate was not modified in
the transaction and not modified outside the transaction to beyond the
start generation of the transaction. If the modified generation is
higher than the transaction start generation, this generation is
reported.
bugNote that the above implies
that inside a transaction we observe a changing last modified generation
for predicates that have only been modified outside the transaction
while these changes are not visible.
- The transaction start generation plus the local generation of the last change if the predicate is modified inside the transaction.
- The global modified generation if the predicate was not modified in
the transaction and not modified outside the transaction to beyond the
start generation of the transaction. If the modified generation is
higher than the transaction start generation, this generation is
reported.
bugNote that the above implies
that inside a transaction we observe a changing last modified generation
for predicates that have only been modified outside the transaction
while these changes are not visible.
- Wait for database changes
- The predicate thread_wait/2 does not wakeup threads for changes inside a transaction. The wakeup is delayed until the transaction is committed. Note that thread_wait/2 cannot be meaningfully called from inside a transaction because no external entities can cause changes to the dynamic database inside the transaction.
- Incremental tabling
- Consistency of tables must be restored if the transaction is rolled
back. For local tables this is realised as follows:
- Tables are either marked to be invalidated on rollback or, for monotonic tabling individual answers are marked to be removed on rollback.
- A table is marked to be invalidated if, while it is created or reevaluated, at least one dependent dynamic predicate has been modified inside the transaction.
- Answers are marked to be retracted when they result from monotonic reevaluation based on changes inside the transaction.
In other words: tables being reevaluated inside a transaction that do not depend on predicates modified inside the transaction remain valid. Monotonic tables that get new answers due to asserts inside the transaction have these answers removed during the rollback while the table remains valid. Monotonic tables that are for some reason invalidated inside the transaction are invalidated during the rollback.
Correct interaction between tabling and transaction currently only deals with local tables. Shared tables should not be combined with transactions. Future versions may improve on that. A possible route is to make a local copy from a shared table when (re)evaluation is performed inside a transaction.
Status SWI-Prolog transaction basics and API are stable. Interaction with other parts of the system that depend on dynamic predicates is still unsettled. Future versions may support non-determinism through transactions and snapshots.