defmodule Wongi.Engine do @moduledoc """ This is a pure-Elixir forward chaining inference engine based on the classic Rete algorithm. It's derived from an earlier [Ruby library](ruby.md) and has a similar interface. ## Usage The following examples will assume a prelude of ```elixir import Wongi.Engine import Wongi.Engine.DSL ``` which comprises the entire public interface of the library. First, an engine instance needs to be created: ```elixir engine = new() ``` ## Knowledge management All knowledge in the system is represented as a set of triples in the form of `{subject, predicate, object}`. Any complex properties or relationships between entities can be broken down to this form, in which case the relationship members will take on the role of subjects and objects, and the type of the relationship will be the predicate. Any Elixir term except the atom `:_` can be used as any element of a triple, although predicates naturally tend to be atoms. References can be used to naturally represent anonymous graph nodes. Facts are added into the system with `assert/2` or `assert/4`: ```elixir engine = engine |> assert(:earth, :satellite, :moon) # or engine = engine |> assert({:earth, :satellite, :moon}) # or engine = engine |> assert([:earth, :satellite, :moon]) # or engine = engine |> assert(WME.new(:earth, :satellite, :moon)) ``` WME (working memory element) is the standard Rete term for "fact". You would rarely need to construct `Wongi.Engine.WME` instances by hand, but you might retrieve them from the engine and use in further function calls. Similarly, `retract/2` or `retract/4` remove facts from the system. ### Searching `select/2` and `select/4` can be used to return a set of facts matching a template. A template is a triple where some of the elements can be the special placeholder value `:_`. An enumerable of all facts matching the template is returned: ```elixir [fact] = engine |> select(:earth, :satellite, :_) |> Enum.to_list() IO.inspect(fact.object) # => :moon ``` ## Rules Rules allow expressing more complex conditions than a single template. A rule is constructed like this: ```elixir rule = rule("optional name", forall: [ matcher1, matcher2, ... ]) IO.inspect(rule.ref) # => #Reference<...> ``` The `ref` field is going to be used later to retrieve the results of rule execution. The rule can then be installed into the engine: ```elixir engine = engine |> compile(rule) ``` Alternatively, this form can be used if you don't want an intermediate variable for the rule, although it is less pipeable: ```elixir {engine, ref} = engine |> compile_and_get_ref(rule(forall: [...])) ``` The `forall` section of a rule consists of a list of matchers (more fully documented in `Wongi.Engine.DSL`) that express some sort of condition. The simplest matcher is `Wongi.Engine.DSL.has/3` which passes if a fact matching its template exists. A crucial part of matching is the variable bindings. A variable is specified using `Wongi.Engine.DSL.var/1`. The first time a variable is encountered, it is bound to the matched value. Subsequent matches will only succeed if the value is the same as the initially bound one. `:_` can be used as a placeholder variable that matches anything and is not bound to any value. ```elixir rule = rule(forall: [ has(:_, :satellite, var(:satellite)), has(var(:satellite), :mass, var(:mass)) ]) engine = new() |> compile(rule) |> assert(:earth, :satellite, :moon) |> assert(:moon, :mass, 7.34767309e22) ``` The results of rule execution can be retrieved using `tokens/2`, which returns an enumerable. A token represents a single possible execution of the matcher sequence. Our set of facts satisfies the rule exactly once, so we expect exactly one token. The bound variables can then be inspected on it: ```elixir [token] = engine |> tokens(rule.ref) |> Enum.to_list() IO.inspect(token[:satellite]) # => :moon IO.inspect(token[:mass]) # => 7.34767309e22 ``` ## Generation In addition to passively examining the results, it is also possible for a rule to perform some actions when it is fully satisfied. Generating additional facts is one such action. For example, we can add a rule that generates a fact about the gravitational pull on the satellite: ```elixir rule = rule( forall: [ has(var(:planet), :satellite, var(:satellite)), has(var(:planet), :mass, var(:planet_mass)), has(var(:satellite), :mass, var(:sat_mass)), has(var(:satellite), :distance, var(:distance)), assign(:pull, &(6.674e-11 * &1[:sat_mass] * &1[:planet_mass] / :math.pow(&1[:distance], 2))) ], do: [ gen(var(:satellite), :pull, var(:pull)) ] ) engine = new() |> compile(rule) |> assert(:earth, :satellite, :moon) |> assert(:earth, :mass, 5.972e24) |> assert(:moon, :mass, 7.34767309e22) |> assert(:moon, :distance, 384_400.0e3) [wme] = engine |> select(:moon, :pull, :_) |> Enum.to_list() IO.inspect(wme.object) # => 1.9819334566450407e20 ``` The generated facts keep track of the rule that generated them and get automatically retracted if the conditions are no longer satisfied. If a fact has been generated by a rule and also asserted manually, it also needs to be retracted by both means to be removed from the system. """ alias Wongi.Engine.Entity alias Wongi.Engine.Rete @type t() :: Wongi.Engine.Rete.t() @type wme() :: Wongi.Engine.WME.t() @type fact() :: {any(), any(), any()} | wme() @type template() :: {any(), any(), any()} | wme() @type rule() :: Wongi.Engine.DSL.Rule.t() @doc """ Creates a new engine instance. """ @spec new() :: t() defdelegate new(), to: Rete @doc """ Returns an engine with the given rule installed. See `Wongi.Engine.DSL` for details on the rule definition DSL. """ @spec compile(t(), rule()) :: t() defdelegate compile(rete, rule), to: Rete @doc """ Returns an engine with the given rule installed and the rule reference. The rule reference can be used to retrieve production tokens using `tokens/2`. See `Wongi.Engine.DSL` for details on the rule definition DSL. """ @spec compile_and_get_ref(t(), rule()) :: {t(), reference()} defdelegate compile_and_get_ref(rete, rule), to: Rete @doc "Returns an engine with the given fact added to the working memory." @spec assert(t(), fact()) :: t() defdelegate assert(rete, fact), to: Rete @doc "Returns an engine with the given fact added to the working memory." @spec assert(t(), any(), any(), any()) :: t() defdelegate assert(rete, subject, predicate, object), to: Rete @doc "Returns an engine with the given fact removed from the working memory." @spec retract(t(), fact()) :: t() defdelegate retract(rete, fact), to: Rete @doc "Returns an engine with the given fact removed from the working memory." @spec retract(t(), any(), any(), any()) :: t() defdelegate retract(rete, subject, object, predicate), to: Rete @doc "Returns a set of all facts matching the given template." @spec select(t(), template()) :: MapSet.t(fact()) defdelegate select(rete, template), to: Rete @doc "Returns a set of all facts matching the given template." @spec select(t(), any(), any(), any()) :: MapSet.t(fact()) defdelegate select(rete, subject, predicate, object), to: Rete @doc "Returns a set of all tokens for the given production node reference." @spec tokens(t(), reference()) :: MapSet.t(Wongi.Engine.Token.t()) defdelegate tokens(rete, node), to: Rete @doc "Returns all production node references." @spec productions(t()) :: MapSet.t(reference()) defdelegate productions(rete), to: Rete def entity(rete, subject), do: Entity.new(rete, subject) end