Prolog from the ground up

A knowledge base that grows

Adding and removing facts while the program runs, the dynamic directive that allows it, and the add-once rule that keeps a list from filling with copies.

Part 11 of 12

Advanced20 min readBack to Courses

Facts that arrive while the program runs

A knowledge base can be changed from inside the program. assertz/1 adds a fact at the end of a predicate, and retract/1 removes the first fact that matches. Between them the program keeps its state as things that are true, rather than as values passed from hand to hand.

One rule is worth knowing from the start. To add a fact only when it is not there yet, ask whether it is already there and commit to that answer with a cut. Without the cut the same fact can be added twice, and every later answer doubles.

assertz adds at the end and asserta adds at the front. The difference is the order a later search meets the clauses, which matters when the predicate has rules as well as facts. For a set of facts with no order, either works; for a queue or a stack, the choice is the whole point.

Marking a predicate as changing

A predicate that the file defines and the program later changes has to be declared with a dynamic directive, which names the predicate and its arity: :- dynamic(task/1). Without that line the engine refuses the change with a permission error, which is a useful reminder that the facts in a file are normally fixed.

Reading the base back is only a matter of asking it questions. A findall goal hands you the current contents as a list, and a negated goal tells you whether something was ever recorded.

The change is visible to every call made after it, not only to the clause that made it. A predicate that was empty can become non-empty halfway through a run, and a negated goal asked before the assert can succeed while the same goal asked after it fails. That is why the order of the goals around a change matters as much as the change itself.

Adding something only once

:- dynamic(seen/1).

mark(X) :-
    seen(X), !.
mark(X) :-
    assertz(seen(X)).

main :-
    mark(tea),
    mark(tea),
    findall(X, seen(X), L),
    write('seen once '), write(L), nl.
:- initialization(main).

Reading the base back

findall(Template, Goal, List) collects every answer to a goal into one list, which turns a search into a value you can print or pass on. \+ Goal succeeds when the goal cannot be proved — it is the “not” of Prolog, and the backslash is part of the operator.

retract/1 removes the first matching fact, not every copy. If a predicate may hold duplicates, count them before deleting, or use retractall/1 when removing all of them is what you mean.

If the base holds task(write_notes) twice, retract(task(write_notes)) removes one and leaves one, while retractall(task(write_notes)) removes both. After a retract, a findall that asked for the tasks returns the shorter list, which is the quickest way to see what changed.

A dynamic predicate is changed for the rest of the session. If a later example depends on the original facts, it must reset them or run in a fresh page; the page keeps the state between runs.

State, or just more facts

It is tempting to treat a dynamic predicate as a mutable variable. It is more useful to see it as a growing set of facts: the program asks what is true now, and the same rule answers whether the set has changed. That keeps the logic in the rules and the state in the facts, instead of spreading both through a chain of assignments.

The dynamic directive and the cut are the two pieces that make the set behave: one allows change, the other prevents duplicates. Without the directive the engine refuses the change; without the cut the same fact can be added twice.

The page keeps the base between runs, so a fact asserted by one run is still there in the next. That is convenient for experimenting and confusing when a later run expects a clean file. A reset goal at the start of main, or a fresh page, is the honest way to begin from the facts as written.

Try these next

  • Add a remove/1 rule that retracts a task exactly once.
  • Use findall to print the tasks before and after a change.
  • Ask \+ seen(coffee) before and after adding coffee.

A list that grows and shrinks

Run it and compare the two lists. Then remove the cut from the add rule, add the same task twice, and see what the list looks like.

Output
Click "Run" to execute your code

Ask what is not there

Run it, then mark coffee first and run again. The line about coffee should disappear.

Output
Click "Run" to execute your code

One retract removes one matching fact, so a predicate that holds duplicates needs one call per copy. Ask whether the duplicates were wanted before you start deleting.

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What does assertz/1 do?