lib/wongi/engine.ex

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