Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/HarvardPL/AbcDatalog/llms.txt

Use this file to discover all available pages before exploring further.

Rather than waiting for evaluation to finish and then inspecting a result set, a DatalogListener lets your application respond to new facts the moment they are derived. You associate a listener with a predicate symbol before evaluation starts; the executor then calls the listener’s method each time a new fact with that predicate is produced. This is the primary mechanism for building reactive logic on top of a running Datalog evaluation.

The DatalogListener interface

DatalogListener is a single-method interface:
public interface DatalogListener {
    void newFactDerived(PositiveAtom fact);
}
Because DatalogListener has exactly one abstract method, you can supply any lambda or method reference wherever a DatalogListener is expected. The fact argument passed to newFactDerived is always a ground atom — fact.isGround() is guaranteed to be true.

Registering a listener

Call registerListener(PredicateSym p, DatalogListener listener) on the executor after initialize() but before start():
ex.registerListener(tc, fact -> System.out.println("Fact derived: " + fact));
Key rules:
  • Must be called before start(). Calling registerListener after the evaluation has started throws IllegalStateException.
  • Scoped to a predicate symbol. The listener fires only for facts whose predicate symbol matches p. Facts with other predicates are not delivered to that listener.
  • Multiple listeners per predicate. You can register more than one listener for the same predicate symbol. Each registered listener will be invoked independently.

Example: two listeners from ExecutorExample

The following excerpt registers one listener to print every derived transitive-closure fact and a second listener to detect when a cycle is found:
// Every time a new tuple is added to the transitive closure relation,
// print it out.
ex.registerListener(
    tc,
    fact -> {
      synchronized (System.out) {
        System.out.println("Fact derived: " + fact);
      }
    });

// Notify us if a cycle is detected.
ex.registerListener(
    cycle,
    fact -> {
      synchronized (System.out) {
        System.out.println("*** Cycle detected. ***");
      }
    });
The cycle predicate is zero-arity (cycle :- tc(X,X).), so its listener fires at most once — the moment the evaluator concludes that a cycle exists in the graph. The tc listener fires once per newly derived transitive-closure tuple.

Thread safety

Listeners are invoked in arbitrary threads managed by the executor. Two important consequences follow from this:
  • Do not block inside a listener. A blocking listener holds up the thread that called it, which can stall evaluation. Keep listener bodies short and non-blocking.
  • Synchronize shared state. If a listener writes to a resource that might be accessed from multiple threads (such as the standard output stream or a shared collection), you must synchronize that access explicitly.
The example above uses synchronized (System.out) before writing to the console. Without this synchronization, output from concurrent listener invocations can interleave unpredictably. Apply the same discipline to any mutable state your listeners touch.
ex.registerListener(
    tc,
    fact -> {
      synchronized (System.out) {
        System.out.println("Fact derived: " + fact);
      }
    });

Collecting results from listeners

When you need to accumulate all facts that a listener receives, use a thread-safe collection. Collections.synchronizedSet or ConcurrentHashMap-backed sets are straightforward choices:
Set<PositiveAtom> derivedFacts = Collections.synchronizedSet(new HashSet<>());
ex.registerListener(tc, fact -> derivedFacts.add(fact));
Access the collection only after shutdown() completes if you need a consistent snapshot, since listeners may continue firing until shutdown finishes.

Using a listener as a termination condition

If your program has a predicate that signals a goal condition — such as a zero-arity cycle predicate — you can call shutdown() from inside the listener to stop evaluation as soon as the condition is reached. This avoids processing additional facts that are no longer needed.
ex.registerListener(
    cycle,
    fact -> {
      synchronized (System.out) {
        System.out.println("*** Cycle detected. ***");
      }
      ex.shutdown();
    });
Because shutdown() is called from a listener thread, make sure any state your listener reads after shutdown is also properly synchronized.

Build docs developers (and LLMs) love