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, and each bucket is a Rete.Bucket — the
same tombstoned ordered multiset working memory keys per join key. add/2, pop/1 and
remove/2 are all O(1) amortised. remove/2 used to be linear in one bucket, which is
one rule's pending matches, so retracting the support of a rule with many of them was
quadratic. See docs/design/engine.md §7.
What makes remove/2 O(1) is the bucket's index, and a bucket builds that only when
something is first taken from it. An agenda that is only ever added to and drained —
a session that never retracts — never builds one.
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
@type key() :: {integer(), integer(), non_neg_integer()}
@type t() :: %Rete.Agenda{ buckets: %{required(key()) => Rete.Bucket.t()}, keys: [key()], size: non_neg_integer() }
Functions
@spec add(t(), Rete.Activation.t()) :: t()
Adds an activation, behind the ones already queued for its rule.
@spec new() :: t()
An empty 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
@spec pop(t()) :: {:ok, Rete.Activation.t(), t()} | :empty
Takes the most salient activation, or :empty.
iex> Rete.Agenda.pop(Rete.Agenda.new())
:empty
@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
@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
@spec to_list(t()) :: [Rete.Activation.t()]
Every pending activation, in firing order.