Rete.Engine (Rete v0.5.0)

Copy Markdown View Source

The propagation loop and the fire cycle.

Internal. Not part of the public API. Call it through Rete.Session.

Propagation drains a queue of pending work. A node consumes one unit, and returns the work it produced. Firing pops the most salient activation, runs its right hand side, and inserts what it returned. Propagation drains to completion before the next activation fires, so a rule always sees a settled network.

fire_rules/2 is the only entry point that drains. new/1, insert/3 and retract/3 queue work and return.

fire_rules/2 returns at quiescence. Every rule whose left hand side holds has fired, and nothing whose support has gone is still asserting anything.

See docs/design/engine.md §2 for the loops, §8 for truth maintenance, and docs/design/observability.md §3 for the loop guard.

Summary

Functions

Every fact the session holds, inserted or concluded.

Fires until the agenda is empty.

Records facts and queues their propagation.

The state a listener has accumulated, or nil if it is not attached.

A state over a network, with nothing inserted.

Runs a query: one result per match, computed by the query's body.

Which index filters would use at a query, or :scan.

Removes facts and queues the retraction.

Whether the state has no propagation queued.

Attaches a listener with its initial state.

Functions

facts(state)

@spec facts(Rete.Engine.State.t()) :: [term()]

Every fact the session holds, inserted or concluded.

This excludes the marker facts an extracted compound negation inserts. They express a negated conjunction to the network, and no rule of the user's concluded them. Everywhere else, they are ordinary facts.

fire_rules(state, opts \\ [])

@spec fire_rules(
  Rete.Engine.State.t(),
  keyword()
) :: Rete.Engine.State.t()

Fires until the agenda is empty.

Options:

  • :max_cycles — how many cycles one call may fire. A cycle is one pass of the fire loop: one activation at the default concurrency, one whole activation group above it. :infinity by default, so an oscillating ruleset spins rather than raising. Firing that many and still having work pending raises with the rules that fired most. Firing that many and settling is fine. See docs/design/observability.md §3.
  • :concurrency — how many rule bodies of one activation group run at once. 1 by default, which is the sequential path. Above 1, the bodies of a group run on tasks and their conclusions are applied in group order. Worth raising only when a body is expensive: a body that just builds a tuple is about 1.5% of firing, and a task costs more than that. See docs/design/engine.md §11.
  • :timeout — milliseconds a single body may take, or :infinity, the default. Only applies when :concurrency is above 1.

insert(state, facts, origin \\ :asserted)

Records facts and queues their propagation.

A fact equal to one already present bumps its count and queues nothing. The matches it would make already exist.

This does not propagate. Rete.Memory holds the fact at once, so facts/1 sees it. The alpha work waits in the queue until fire_rules/2 drains it. See docs/design/engine.md §2.

listener_state(state, module)

@spec listener_state(Rete.Engine.State.t(), module()) :: term()

The state a listener has accumulated, or nil if it is not attached.

new(network)

A state over a network, with nothing inserted.

Queues the root token rather than propagating it. A rule whose whole left hand side is an absence or an empty collection is true of the empty session. So the token must exist before a fact arrives. fire_rules/2 is what propagates it. A state nobody has fired holds no matches and no activations. See docs/design/engine.md §6.

query(state, ref, filters \\ [])

@spec query(
  Rete.Engine.State.t(),
  {module(), atom()},
  keyword() | %{required(atom()) => term()}
) :: [
  term()
]

Runs a query: one result per match, computed by the query's body.

A query is named by the {module, name} pair it was defined under. defquery summary(...) also defines summary/2 in its own module. MyRuleset.summary(session, filters) is the readable form of this call.

filters narrows the matches by equality on the bindings, before the body runs. It may name any variable the left hand side binds.

Row order is unspecified. Rows follow the order the facts arrived in.

This used to sort every result, so that one fact set always answered the same way. The contract never promised that, and the sort cost O(n log n) on every call. The rows are the same without it. Only their sequence moves.

query_plan(state, ref, filters \\ [])

@spec query_plan(
  Rete.Engine.State.t(),
  {module(), atom()},
  keyword() | %{required(atom()) => term()}
) ::
  {:index, [atom()]} | :scan

Which index filters would use at a query, or :scan.

A declared index that no call ever uses is silently no faster, which is the one thing that cannot be seen from the outside. This says so. See Rete.Inspect.query_plan/3.

retract(state, facts, origin \\ :asserted)

Removes facts and queues the retraction.

Only the last occurrence of a fact queues anything. The engine retracts anything concluded from it in turn, once fire_rules/2 drains the queue and the network settles.

This does not propagate, for the reason insert/3 gives. Queuing an insert and then a retract of the same fact drains to a net no-op. The queued work for one node keeps the order it arrived in, whatever a caller put between the two calls.

settled?(state)

@spec settled?(Rete.Engine.State.t()) :: boolean()

Whether the state has no propagation queued.

false means a fact reached working memory and no node has seen it yet. A state fresh from new/1 is unsettled, because the root token is queued too. See docs/design/engine.md §2.

This reads the queue alone, and that is enough. On every state a caller can hold, the agenda is empty whenever the queue is: fire_loop/4 returns only on :empty, and every other way out of a fire raises, which discards the state. So there is no state with work queued nowhere and an activation still waiting. Keep it that way, or this answers true about a state that has yet to fire something.

with_listener(state, module, init)

@spec with_listener(Rete.Engine.State.t(), module(), term()) :: Rete.Engine.State.t()

Attaches a listener with its initial state.