Rete.Memory (Rete v0.2.0)

Copy Markdown View Source

Working memory: everything a session knows, as one immutable value.

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

Five memories, plus one flag:

elements    node_id => join_key => Bucket of Element   right of a beta node
tokens      node_id => join_key => Bucket of Token     left of a beta node
accum       node_id => join_key => group_key => [fact]
insertions  node_id => token => [[fact]]               truth maintenance
facts       fact => count                              what it was told

Three properties are load-bearing. See docs/design/engine.md §4.

  • Arrival order. It decides the order tokens propagate, and so the order two matches of one rule fire. A bucket that gave items back in a different order would reorder every :activation_fired event.
  • Removal collapses the level above. Every key above the leaf is a value, so an entry pointing at an empty leaf leaks. Rete.Engine.Nodes also needs "no group" and "an empty group" to stay different answers.
  • Multisets, not sets. Inserting a fact twice, then retracting once, must leave it present. Two rules may each have concluded it.

root_seeded? is not a memory. It records that the beta root's empty token has been planted. This must happen exactly once per session. See docs/design/engine.md §6.

iex> alias Rete.Memory
iex> {memory, :new} = Memory.add_fact(Memory.new(), {:order, 1})
iex> {memory, :duplicate} = Memory.add_fact(memory, {:order, 1})
iex> {memory, :remaining} = Memory.remove_fact(memory, {:order, 1})
iex> Memory.facts(memory)
[{:order, 1}]

Summary

Functions

Adds elements at a node under a join key.

Records a fact, returning {memory, :new | :duplicate}.

Records the facts one activation of a production inserted.

Adds tokens at a node under a join key.

Every element at a node, whatever its join key.

Every token at a node, whatever its join key.

Drops a collection group entirely.

The whole memory as plain data, every bucket rendered as a list in arrival order.

The elements stored at a node under a join key, in arrival order.

Every distinct fact the session holds.

The collection groups at a node under a join key, group_key => facts.

Records that the beta root's empty token has been propagated.

An empty memory.

Replaces the facts of one collection group.

Removes one occurrence of each given element, returning {memory, removed}.

Drops one occurrence of a fact, returning {memory, :gone | :remaining | :absent}.

Removes one occurrence of each given token, returning what was found.

Takes back one batch of facts a token's activation inserted.

The tokens stored at a node under a join key, in arrival order.

Types

key()

@type key() :: %{required(atom()) => term()}

node_id()

@type node_id() :: term()

t()

@type t() :: %Rete.Memory{
  accum: %{
    required(node_id()) => %{required(key()) => %{required(key()) => [term()]}}
  },
  elements: %{
    required(node_id()) => %{required(key()) => Rete.Memory.Bucket.t()}
  },
  facts: %{required(term()) => pos_integer()},
  insertions: %{
    required(node_id()) => %{required(Rete.Token.t()) => [[term()]]}
  },
  root_seeded?: boolean(),
  tokens: %{required(node_id()) => %{required(key()) => Rete.Memory.Bucket.t()}}
}

Functions

add_elements(memory, node_id, key, new)

@spec add_elements(t(), node_id(), key(), [Rete.Element.t()]) :: t()

Adds elements at a node under a join key.

add_fact(memory, fact)

@spec add_fact(t(), term()) :: {t(), :new | :duplicate}

Records a fact, returning {memory, :new | :duplicate}.

Only :new propagates. A second insertion of an equal fact bumps its count instead, so that one retraction does not remove it. The matches it would make already exist.

add_insertion(memory, node_id, token, facts)

@spec add_insertion(t(), node_id(), Rete.Token.t(), [term()]) :: t()

Records the facts one activation of a production inserted.

This is stored as a list of lists. The same token can activate a production more than once, over a session's life, and each activation owns its own batch.

add_tokens(memory, node_id, key, new)

@spec add_tokens(t(), node_id(), key(), [Rete.Token.t()]) :: t()

Adds tokens at a node under a join key.

all_elements(memory, node_id)

@spec all_elements(t(), node_id()) :: [Rete.Element.t()]

Every element at a node, whatever its join key.

all_tokens(memory, node_id)

@spec all_tokens(t(), node_id()) :: [Rete.Token.t()]

Every token at a node, whatever its join key.

drop_group(memory, node_id, key, group_key)

@spec drop_group(t(), node_id(), key(), key()) :: t()

Drops a collection group entirely.

A group with no facts is not the same as a group holding []. The first does not exist. The second is an empty collection the rule can legitimately see. Only a grouping collection ever drops to nothing.

The join key that held the last group goes with it, and so does the node, if that was its last join key. Both are binding values, so leaving them behind would leak one entry per entity the session has seen.

dump(memory)

@spec dump(t()) :: %{
  elements: %{required(node_id()) => %{required(key()) => [Rete.Element.t()]}},
  tokens: %{required(node_id()) => %{required(key()) => [Rete.Token.t()]}},
  accum: %{
    required(node_id()) => %{required(key()) => %{required(key()) => [term()]}}
  },
  insertions: %{
    required(node_id()) => %{required(Rete.Token.t()) => [[term()]]}
  },
  facts: %{required(term()) => pos_integer()},
  root_seeded?: boolean()
}

The whole memory as plain data, every bucket rendered as a list in arrival order.

Use this instead of the struct. A Rete.Memory.Bucket holds a stack with tombstones in it, and two memories that agree on every match can still disagree there. This is the view that is meaningful to compare, assert on, and write down.

elements(memory, node_id, key)

@spec elements(t(), node_id(), key()) :: [Rete.Element.t()]

The elements stored at a node under a join key, in arrival order.

facts(memory)

@spec facts(t()) :: [term()]

Every distinct fact the session holds.

groups(memory, node_id, key)

@spec groups(t(), node_id(), key()) :: %{required(key()) => [term()]}

The collection groups at a node under a join key, group_key => facts.

mark_root_seeded(memory)

@spec mark_root_seeded(t()) :: t()

Records that the beta root's empty token has been propagated.

Rete.Engine.Nodes seeds only while this is false. So a session plants exactly one root token, however many times it is asked.

new()

@spec new() :: t()

An empty memory.

put_group(memory, node_id, key, group_key, facts)

@spec put_group(t(), node_id(), key(), key(), [term()]) :: t()

Replaces the facts of one collection group.

remove_elements(memory, node_id, key, targets)

@spec remove_elements(t(), node_id(), key(), [Rete.Element.t()]) ::
  {t(), [Rete.Element.t()]}

Removes one occurrence of each given element, returning {memory, removed}.

An element that was not there is left out of removed. So a caller can tell a real retraction from a no-op. Propagating a retraction that never happened would corrupt the counts downstream.

remove_fact(memory, fact)

@spec remove_fact(t(), term()) :: {t(), :gone | :remaining | :absent}

Drops one occurrence of a fact, returning {memory, :gone | :remaining | :absent}.

Only :gone propagates — that is, only the last occurrence.

remove_tokens(memory, node_id, key, targets)

@spec remove_tokens(t(), node_id(), key(), [Rete.Token.t()]) ::
  {t(), [Rete.Token.t()]}

Removes one occurrence of each given token, returning what was found.

take_insertion(memory, node_id, token)

@spec take_insertion(t(), node_id(), Rete.Token.t()) :: {t(), [term()]}

Takes back one batch of facts a token's activation inserted.

Returns {memory, facts}, or {memory, []} when the token never inserted anything. That case is a production retracted before it fired, or one whose body returned nothing.

tokens(memory, node_id, key)

@spec tokens(t(), node_id(), key()) :: [Rete.Token.t()]

The tokens stored at a node under a join key, in arrival order.