Rete.Ruleset (Rete v0.5.0)

Copy Markdown View Source

Macros for defining rulesets in a Rete network.

A rule reads as a function. Its arguments are the left hand side, and its body is the right hand side. Pattern matching in the argument list gives you destructuring, variable binding, and join-variable identification for free. What the body returns is the facts to insert. docs/dsl.md is the guide.

defmodule MyRuleset do
  use Rete.Ruleset

  derive(:dog, :mammal)

  defrule loyalty(%{salience: 100}, {:customer, cid, name}, orders = [{:order, cid, _amt}]) do
    {:loyalty, cid, name, length(orders)}
  end
end

Using this module makes the ruleset expose get_rule_data/0, get_expr_data/0, get_taxo_data/0, and get_version/0. Rete aggregates these across modules. It also defines <query_name>/1,2 for each query, which is the public face of a query, plus the __rhs_<name>__/2 and __<expr_code>__/1,2 machinery the engine calls.

Every defrule and defquery expands by running the front end pipeline:

Rete.DSL.Parser      parse the quoted declaration into Rete.IR
Rete.DSL.Normalize   rewrite gates into conditions, negations and :or
Rete.Compiler.Sort   order the conditions so every join has its keys
Rete.DSL.Bindings    classify join/new bindings, split guards
build/4              recompute the production's :bind from the result
Rete.DSL.Codegen     emit the expression functions and the RHS

See docs/design/ir.md §1 for the contract between the phases.

Summary

Functions

Runs the front end pipeline over a quoted production declaration.

Rejects a production name the module has already used, and records it.

Defines a query.

Defines a rule.

Declares that child is a kind of parent.

Declares an index over a query's bindings.

Records one index/2 declaration, checking its shape.

Records the bindings of a query, so resolve_indexes!/1 can check an index against them.

Resolves every recorded index/2 against the queries of a module.

Removes a derivation declared earlier.

Puts each query's declared indexes into its :opts.

Functions

build(env, decl, body, type)

@spec build(Macro.Env.t(), Macro.t(), Macro.t(), :rule | :query) ::
  Rete.IR.Production.t()

Runs the front end pipeline over a quoted production declaration.

Returns the fully classified Rete.IR.Production, ready for Rete.DSL.Codegen.compile/1. This is exposed so a test can inspect the IR of a declaration, without compiling a module for it.

The last step recomputes :bind from the classified LHS. So :bind is exactly the set of variables a token reaching the right hand side can carry.

check_name!(module, name, type, file, line)

@spec check_name!(module(), atom(), :rule | :query, String.t(), pos_integer()) :: :ok

Rejects a production name the module has already used, and records it.

The compiler calls this from the module body, not at macro expansion. A module body is expanded in full before any of it is evaluated. So at expansion time, the attribute that records earlier declarations is still empty, and every declaration would look like the first.

Rules and queries share one namespace.

defquery(decl, body)

(macro)

Defines a query.

A query has the same left hand side as a rule, but it never fires. It holds the matches that reached it. Its body is what the caller gets, one result per match.

The query is a function. defquery find_user(...) also defines find_user/1,2 in the same module, so you run it by calling it. That is what makes a query addressable, and why two rulesets may each define one of the same name. Use Rete.Session.query/3, with {MyRuleset, :find_user}, when the query is decided at runtime.

There is nothing to declare about parameters. The caller may constrain any variable the left hand side binds. Filtering happens on the bindings, before the body runs. A filter that names something the query does not bind raises an error, instead of answering [].

defquery find_user({:user, id, name}) do
  {id, name}
end
#=> MyRuleset.find_user(session)         [{1, "Ada"}]
#=> MyRuleset.find_user(session, id: 1)  [{1, "Ada"}]

defrule(decl, body)

(macro)

Defines a rule.

The declaration is the left hand side, and the body is the right hand side. The engine logically inserts and truth-maintains what the body returns. nil or [] inserts nothing.

{:user, id}                      fact pattern, any arity, including {:tick}
%User{id: id}                    struct fact pattern, the type is the module
%{__type__: :user, id: id}       tagged map fact pattern
user = {:user, id}               bind the whole fact
{:order, total} when total > 10  per condition guard
orders = [{:order, id}]          collect all matching facts, bound or anonymous
{:not, [{:order, id}]}           gate: :and :or :not :nand :nor :xor :xnor

A %{...} literal in first position is the rule's options, not a condition. A when after the argument list is a guard over all bindings. See docs/dsl.md.

defrule high_value(%{salience: 100}, {:user, id}, {:order, id, t} when t > 1000) do
  {:high_value, id, t}
end

derive(child, parent)

(macro)

Declares that child is a kind of parent.

A child fact then reaches every condition written against parent. The reverse does not hold. Derivation is transitive.

derive(:dog, :mammal)
derive(:mammal, :animal)

# a {:dog, "Rex"} fact now matches this rule
defrule process_animal({:animal, name}), do: {:seen, name}

index(name, keys)

(macro)
@spec index(atom(), [atom()]) :: Macro.t()

Declares an index over a query's bindings.

A query answers from a scan of every match it holds. An index buckets those matches by the bindings named here, so a call that filters on exactly those bindings, or on a superset of them, reads one bucket instead of all of them.

defquery flagged_for({:flagged, cid, tid, amt}) do
  {cid, tid, amt}
end

index :flagged_for, [:cid]
index :flagged_for, [:cid, :tid]

[:cid, :tid] is one index over both bindings, not two. Write two lines for two indexes. Order within the list does not matter.

An index changes speed, not results. Every filter still works, indexed or not, and returns the same rows in the same order. Declaring none is the default, and costs nothing. This declares no parameters and permits nothing: the caller may still filter on any variable the left hand side binds.

A declaration may come before or after the query it names. Both are resolved when the module finishes compiling.

record_index!(module, name, keys, file, line)

@spec record_index!(module(), atom(), [atom()], String.t(), pos_integer()) :: :ok

Records one index/2 declaration, checking its shape.

Called from the module body rather than at macro expansion, for the reason check_name!/5 gives: a module body is expanded in full before any of it runs, so at expansion time the attribute holding earlier declarations is still empty.

Only the shape is checked here. Whether the name is a query, and whether the keys are bindings of it, cannot be known until every declaration has been seen — an index may come before its defquery. resolve_indexes!/1 does that at @before_compile.

record_query_bind!(module, name, bind)

@spec record_query_bind!(module(), atom(), [atom()]) :: :ok

Records the bindings of a query, so resolve_indexes!/1 can check an index against them.

Plain atoms, kept apart from @rule_data, which holds escaped IR.

resolve_indexes!(module)

@spec resolve_indexes!(module()) :: %{required(atom()) => [[atom()]]}

Resolves every recorded index/2 against the queries of a module.

Returns query name => [key set], in declaration order. Raises when a declaration names something that is not a query of this module, or a binding that query does not have.

underive(child, parent)

(macro)

Removes a derivation declared earlier.

Declarations are folded in module order, so a module can only undo what a module before it declared.

derive(:cat, :mammal)
underive(:cat, :mammal)

with_indexes(productions, indexes)

@spec with_indexes([Rete.IR.Production.t()], %{required(atom()) => [[atom()]]}) :: [
  Rete.IR.Production.t()
]

Puts each query's declared indexes into its :opts.

Runs when get_rule_data/0 is called, rather than at @before_compile, because @rule_data holds escaped IR and only becomes structs when the generated function runs.