Rete.Ruleset (Rete v0.8.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>/(N+1) 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 :bind from the result, then check the head against it
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.

Removes a derivation declared earlier.

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 in the same module, so you run it by calling it. Its arity is one more than the number of patterns in the head. 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.

A head before the conditions is the argument list of that function. It is a list of ordinary Elixir patterns, and a call matches them. The variables they bind are what the engine keys the matches on, and they are the only way to read the query.

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

Any pattern works, so you choose the shape a caller writes:

defquery by_pair(cid, tid)(...)           #=> by_pair(session, 1, 2)
defquery by_tuple({cid, tid})(...)        #=> by_tuple(session, {1, 2})
defquery by_map(%{cid: cid})(...)         #=> by_map(session, %{cid: 1})
defquery by_list(cid: cid, tid: tid)(...) #=> by_list(session, cid: 1, tid: 2)

The session is the first argument, so a query pipes. A head of N patterns gives name/(N+1). A call that does not match raises FunctionClauseError, in the way that any other function does. Because the head is a pattern, a keyword head matches in the order that you declared, and a map head accepts a call with extra keys.

A pattern may carry a guard:

defquery big_sales(cid, amt when amt > 1000)({:sale, cid, amt}) do
  {cid, amt}
end

The guard becomes a test on the left hand side, and nothing else. So the query holds no match that it rejects, and big_sales(session, 1, 5) answers []. It is not on the generated clause, so it may call anything a rule body may call, such as name when String.length(name) > 3.

A head guard reads only what the head binds. Write a guard over the other bindings as a rule level guard instead, after the conditions.

Every head variable is in scope for the guard, whichever pattern you wrote it after. So (cid, tid when cid < tid) compares the two. Several patterns may each carry a guard, and the guards then combine with and. Each pattern takes one when, so write amt when a and b rather than amt when a when b.

A query without a head takes no parameters. It answers with every match that it holds, in arrival order. This is the default, and it costs nothing more.

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

Every variable a head binds must be one the left hand side binds on every match, so a variable that only some branches of a disjunction bind is refused. A head may bind nothing: (:tick) asserts at the call site, and a _-prefixed name labels a position, as in any def. A rule cannot take a head, because you never read a rule. See docs/dsl.md.

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{__type__: :vip, id: id}    a declared type overrides the module
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}

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)