Rete.Agenda (Rete v0.2.0)

Copy Markdown View Source

The activations waiting to fire, most salient first.

Internal. Not part of the public API. It is documented rather than hidden, because durability, checkpointing, and scheduling work will need to reach in here. Treat its functions as liable to change.

Ordering is {salience, internal_salience} descending, then compile order ascending. Two matches of the same rule fire in the order they arrived.

Every activation of one production node shares a sort key. So the agenda is a small number of ordered buckets, not one sorted list. add/2 and pop/1 are O(1). remove/2 is linear in one bucket, which holds one rule's pending matches. See docs/design/engine.md §7.

iex> alias Rete.{Activation, Agenda}
iex> urgent = %Activation{node_id: :n1, salience: 10}
iex> normal = %Activation{node_id: :n2, salience: 0}
iex> agenda = Agenda.new() |> Agenda.add(normal) |> Agenda.add(urgent)
iex> Agenda.to_list(agenda) |> Enum.map(& &1.node_id)
[:n1, :n2]

Summary

Functions

Adds an activation, behind the ones already queued for its rule.

An empty agenda.

Every activation of the most salient group, in firing order, without removing them.

Takes the most salient activation, or :empty.

Removes an activation by value.

How many activations are waiting.

Every pending activation, in firing order.

Types

key()

@type key() :: {integer(), integer(), non_neg_integer()}

t()

@type t() :: %Rete.Agenda{
  buckets: %{required(key()) => :queue.queue(Rete.Activation.t())},
  keys: [key()],
  size: non_neg_integer()
}

Functions

add(agenda, activation)

@spec add(t(), Rete.Activation.t()) :: t()

Adds an activation, behind the ones already queued for its rule.

new()

@spec new() :: t()

An empty agenda.

peek_group(agenda)

@spec peek_group(t()) :: [Rete.Activation.t()]

Every activation of the most salient group, in firing order, without removing them.

A group is every bucket sharing the leading {salience, internal_salience} of the sort key. So it spans the rules that would fire before any less salient one. One group is one cycle of the fire loop, however many activations it holds.

Peeked rather than popped. A caller firing the group removes each activation with remove/2, as it applies it. So an activation that an earlier conclusion in the same group invalidates is still found and cancelled. Taking them all out up front would leave a later retraction nothing to cancel. The conclusion would then be inserted against a token that no longer exists — a fact no retraction could ever take back.

iex> alias Rete.{Activation, Agenda}
iex> agenda =
...>   Agenda.new()
...>   |> Agenda.add(%Activation{node_id: :a, salience: 10, order: 0})
...>   |> Agenda.add(%Activation{node_id: :b, salience: 10, order: 1})
...>   |> Agenda.add(%Activation{node_id: :c, salience: 0, order: 2})
iex> Agenda.peek_group(agenda) |> Enum.map(& &1.node_id)
[:a, :b]
iex> Agenda.size(agenda)
3

pop(agenda)

@spec pop(t()) :: {:ok, Rete.Activation.t(), t()} | :empty

Takes the most salient activation, or :empty.

iex> Rete.Agenda.pop(Rete.Agenda.new())
:empty

remove(agenda, activation)

@spec remove(t(), Rete.Activation.t()) :: {t(), :removed | :missing}

Removes an activation by value.

Returns {agenda, :removed} when the activation was still pending. Returns {agenda, :missing} when it had already fired. The caller must tell the two apart. An activation that never fired inserted nothing, so there is nothing to retract. One that fired has facts that truth maintenance must take back.

iex> alias Rete.{Activation, Agenda}
iex> pending = %Activation{node_id: :n1}
iex> {_agenda, verdict} = Agenda.new() |> Agenda.add(pending) |> Agenda.remove(pending)
iex> verdict
:removed
iex> {_agenda, verdict} = Agenda.remove(Agenda.new(), pending)
iex> verdict
:missing

size(agenda)

@spec size(t()) :: non_neg_integer()

How many activations are waiting.

The agenda counts activations as they arrive, so reporting the size of a runaway agenda is cheap.

iex> alias Rete.{Activation, Agenda}
iex> Agenda.new() |> Agenda.add(%Activation{node_id: :n1}) |> Agenda.size()
1

to_list(agenda)

@spec to_list(t()) :: [Rete.Activation.t()]

Every pending activation, in firing order.