├── CHANGELOG.md
├── test
├── test_helper.exs
├── why_test.exs
├── discovery_test.exs
└── retex_test.exs
├── .formatter.exs
├── lib
├── protocols
│ ├── alpha_network.ex
│ └── activation.ex
├── agenda
│ ├── strategy_behaviour.ex
│ └── execute_once.ex
├── discovery.ex
├── facts
│ ├── relation.ex
│ ├── filter.ex
│ ├── isa.ex
│ ├── is_not.ex
│ ├── facts.ex
│ ├── not_existing_attribute.ex
│ └── has_attribute.ex
├── why.ex
├── nodes
│ ├── root.ex
│ ├── type_select_not.ex
│ ├── negative_type_node.ex
│ ├── type_select.ex
│ ├── type_test.ex
│ ├── p_node.ex
│ ├── type_node.ex
│ └── beta_memory.ex
├── token.ex
├── wme.ex
├── serializer.ex
└── retex.ex
├── .github
└── workflows
│ └── elixir.yml
├── .gitignore
├── config
└── config.exs
├── benchmark
├── rule_chain.exs
└── example.exs
├── mix.exs
├── mix.lock
├── LICENSE
└── README.md
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # tbd
--------------------------------------------------------------------------------
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 |
--------------------------------------------------------------------------------
/.formatter.exs:
--------------------------------------------------------------------------------
1 | [
2 | inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"]
3 | ]
4 |
--------------------------------------------------------------------------------
/lib/protocols/alpha_network.ex:
--------------------------------------------------------------------------------
1 | defprotocol Retex.Protocol.AlphaNetwork do
2 | @doc "This protocol knows how append a new partial production into an existing network based on the type of fact"
3 | def append(fact, accumulator)
4 | end
5 |
--------------------------------------------------------------------------------
/lib/agenda/strategy_behaviour.ex:
--------------------------------------------------------------------------------
1 | defmodule Retex.Agenda.Strategy do
2 | @moduledoc false
3 |
4 | @type rules_executed :: list()
5 | @type network :: Retex.t()
6 |
7 | @callback consume_agenda(rules_executed, network) :: {:ok, {rules_executed, network}}
8 | end
9 |
--------------------------------------------------------------------------------
/lib/protocols/activation.ex:
--------------------------------------------------------------------------------
1 | defprotocol Retex.Protocol.Activation do
2 | @doc "This protocol knows how to activate a node and pass the information to children"
3 | def activate(node, rete, wme, bindings, tokens)
4 |
5 | @doc "Implements the strategy to know if a node is activated or not"
6 | def active?(rete, node)
7 | end
8 |
--------------------------------------------------------------------------------
/lib/discovery.ex:
--------------------------------------------------------------------------------
1 | defmodule Retex.Discovery do
2 | @moduledoc """
3 | Checks which production nodes have been almost activated from the ones
4 | with the most activated paths pointing to them, sorted by weak activations.
5 | """
6 |
7 | defstruct weak_activations: []
8 |
9 | def weak_activations(%Retex{graph: _graph}) do
10 | %__MODULE__{weak_activations: []}
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/lib/facts/relation.ex:
--------------------------------------------------------------------------------
1 | defmodule Retex.Fact.Relation do
2 | @moduledoc "Attribute values that a Wme should have in order for this condition to be true"
3 | defstruct name: nil, from: nil, to: nil, via: nil
4 |
5 | def new(fields) do
6 | via = to_string(fields[:from]) |> String.downcase() |> Kernel.<>("_id")
7 | rel = struct(__MODULE__, fields)
8 | Map.put(rel, :via, via)
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/.github/workflows/elixir.yml:
--------------------------------------------------------------------------------
1 | name: Elixir CI
2 |
3 | on: push
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 |
9 | container:
10 | image: elixir:1.13.1
11 |
12 | steps:
13 | - uses: actions/checkout@v1
14 | - name: Install Dependencies
15 | run: |
16 | mix local.rebar --force
17 | mix local.hex --force
18 | mix deps.get
19 | - name: Run Tests
20 | run: mix test
21 |
--------------------------------------------------------------------------------
/lib/why.ex:
--------------------------------------------------------------------------------
1 | defmodule Retex.Why do
2 | @moduledoc """
3 | Why a rule was activated?
4 |
5 | Use this module passing the conclusion of a rule and it will tell you why it was activated
6 | """
7 |
8 | defstruct conclusion: nil, paths: []
9 | alias Retex.Node.PNode
10 |
11 | def explain(%Retex{graph: graph}, %PNode{} = conclusion) do
12 | conclusion = PNode.new(conclusion.raw_action)
13 | paths = Graph.get_paths(graph, Retex.root_vertex(), conclusion)
14 | %__MODULE__{paths: paths, conclusion: conclusion}
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/facts/filter.ex:
--------------------------------------------------------------------------------
1 | defmodule Retex.Fact.Filter do
2 | @moduledoc "Apply a filter to a variable on the PNode, so that the activation of a PNode happens when this condition is also satisfied"
3 | defstruct variable: nil, predicate: nil, value: nil
4 |
5 | @type variable :: String.t()
6 | @type predicate :: :== | :=== | :!== | :!= | :> | :< | :<= | :>= | :in
7 | @type value :: any()
8 | @type t :: %__MODULE__{variable: variable(), predicate: predicate(), value: value()}
9 | @type fields :: [variable: variable(), predicate: predicate(), value: value()]
10 |
11 | @spec new(fields()) :: t()
12 | def new(fields) do
13 | struct(__MODULE__, fields)
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/nodes/root.ex:
--------------------------------------------------------------------------------
1 | defmodule Retex.Root do
2 | @moduledoc """
3 | The root node is the root vertex of the network.
4 | From the root node start all the edges that connect with each TypeNode,
5 | the discrimination network starts from here.
6 | """
7 | defstruct class: :Root, id: :root
8 |
9 | @type t :: %Retex.Root{}
10 |
11 | def new, do: %__MODULE__{}
12 |
13 | defimpl Retex.Protocol.Activation do
14 | def activate(
15 | %Retex.Root{} = neighbor,
16 | %Retex{graph: _graph} = rete,
17 | %Retex.Wme{attribute: _attribute} = wme,
18 | bindings,
19 | _tokens
20 | ) do
21 | Retex.continue_traversal(rete, bindings, neighbor, wme)
22 | end
23 |
24 | def active?(_node, _rete), do: true
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # The directory Mix will write compiled artifacts to.
2 | /_build/
3 |
4 | # If you run "mix test --cover", coverage assets end up here.
5 | /cover/
6 |
7 | # The directory Mix downloads your dependencies sources to.
8 | /deps/
9 |
10 | # Where third-party dependencies like ExDoc output generated docs.
11 | /doc/
12 |
13 | # Ignore .fetch files in case you like to edit your project deps locally.
14 | /.fetch
15 |
16 | # If the VM crashes, it generates a dump, let's ignore it too.
17 | erl_crash.dump
18 |
19 | # Also ignore archive artifacts (built via "mix archive.build").
20 | *.ez
21 |
22 | # Ignore package tarball (built via "mix hex.build").
23 | rete_elixir-*.tar
24 |
25 | # Ignore elixir language server cache
26 | .elixir_ls/
27 |
28 | # DStore
29 | .DS_Store
30 |
31 |
32 | /priv/plts/*
--------------------------------------------------------------------------------
/test/why_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Retex.WhyTest do
2 | use ExUnit.Case
3 | alias Retex.Facts
4 | import Facts
5 | alias Retex.Why
6 |
7 | defp create_rule(lhs: given, rhs: action) do
8 | %{
9 | given: given,
10 | then: action
11 | }
12 | end
13 |
14 | test "two paths are activated in order to reach a conclusion" do
15 | given = [
16 | has_attribute(:Account, :status, :==, "$a"),
17 | has_attribute(:Family, :size, :==, "$a")
18 | ]
19 |
20 | wme = Retex.Wme.new(:Account, :status, :ok)
21 | wme_2 = Retex.Wme.new(:Family, :size, :ok)
22 |
23 | action = [
24 | {:Flight, :account_status, "$a"}
25 | ]
26 |
27 | rule = create_rule(lhs: given, rhs: action)
28 |
29 | network =
30 | Retex.add_production(Retex.new(), rule) |> Retex.add_wme(wme) |> Retex.add_wme(wme_2)
31 |
32 | action = network.agenda |> List.first()
33 |
34 | assert 2 == Enum.count(Why.explain(network, action) |> Map.get(:paths))
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/lib/facts/isa.ex:
--------------------------------------------------------------------------------
1 | defmodule Retex.Fact.Isa do
2 | @moduledoc "A type of thing that needs to exists in order for a Wme to activate part of a condition"
3 | defstruct type: nil, variable: nil
4 |
5 | @type type :: String.t() | atom()
6 | @type variable :: String.t()
7 | @type fields :: [type: type(), variable: variable()]
8 | @type t :: %__MODULE__{type: type(), variable: variable()}
9 |
10 | @spec new(fields()) :: t()
11 | def new(fields) do
12 | struct(__MODULE__, fields)
13 | end
14 |
15 | defimpl Retex.Protocol.AlphaNetwork do
16 | alias Retex.Fact.Isa
17 | alias Retex.Node.Type
18 |
19 | def append(%Isa{} = condition, {graph, test_nodes}) do
20 | %{variable: _, type: type} = condition
21 | type_node = Type.new(type)
22 |
23 | new_graph =
24 | graph
25 | |> Graph.add_vertex(type_node)
26 | |> Graph.add_edge(Retex.root_vertex(), type_node)
27 |
28 | {new_graph, [type_node | test_nodes]}
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/test/discovery_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Retex.DiscoveryTest do
2 | use ExUnit.Case
3 | alias Retex.Facts
4 | import Facts
5 | alias Retex.Why
6 |
7 | defp create_rule(lhs: given, rhs: action) do
8 | %{
9 | given: given,
10 | then: action
11 | }
12 | end
13 |
14 | test "two paths are activated in order to reach a conclusion" do
15 | given = [
16 | has_attribute(:Account, :status, :==, "$a"),
17 | has_attribute(:Family, :size, :==, "$a")
18 | ]
19 |
20 | wme = Retex.Wme.new(:Account, :status, :ok)
21 | wme_2 = Retex.Wme.new(:Family, :size, :ok)
22 |
23 | action = [
24 | {:Flight, :account_status, "$a"}
25 | ]
26 |
27 | rule = create_rule(lhs: given, rhs: action)
28 |
29 | network =
30 | Retex.add_production(Retex.new(), rule) |> Retex.add_wme(wme) |> Retex.add_wme(wme_2)
31 |
32 | action = network.agenda |> List.first()
33 | assert 2 == Enum.count(Why.explain(network, action) |> Map.get(:paths))
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/lib/facts/is_not.ex:
--------------------------------------------------------------------------------
1 | defmodule Retex.Fact.IsNot do
2 | @moduledoc "A type of thing that needs to not exists in order for a Wme to activate part of a condition"
3 | defstruct type: nil, variable: nil
4 |
5 | @type type :: String.t() | atom()
6 | @type variable :: String.t()
7 | @type fields :: [type: type(), variable: variable()]
8 | @type t :: %__MODULE__{type: type(), variable: variable()}
9 |
10 | @spec new(fields()) :: t()
11 | def new(fields) do
12 | struct(__MODULE__, fields)
13 | end
14 |
15 | defimpl Retex.Protocol.AlphaNetwork do
16 | alias Retex.Fact.IsNot
17 | alias Retex.Node.NegativeType
18 |
19 | def append(%IsNot{} = condition, {graph, test_nodes}) do
20 | %{variable: _, type: type} = condition
21 | type_node = NegativeType.new(type)
22 |
23 | new_graph =
24 | graph
25 | |> Graph.add_vertex(type_node)
26 | |> Graph.add_edge(Retex.root_vertex(), type_node)
27 |
28 | {new_graph, [type_node | test_nodes]}
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/token.ex:
--------------------------------------------------------------------------------
1 | defmodule Retex.Token do
2 | @moduledoc """
3 | The root node of the network is the input to the black box.
4 | This node receives the tokens that are sent to the black box and passes copies
5 | of the tokens to all its successors. The successors of the top node, the nodes to
6 | perform the intra-element tests, have one input and one or more outputs. Each
7 | node tests one feature and sends the tokens that pass the test to its successors.
8 | The two-input nodes compare tokens from different paths and join them into
9 | bigger tokens if they satisfy the inter-element constraints of the LHS. Because
10 | of the tests performed by the other nodes, a terminal node will receive only
11 | tokens that instantiate the LHS. The terminal node sends out of the black box
12 | the information that the conflict set must be changed. - RETE Match Algorithm - Forgy OCR
13 | """
14 | defstruct wmem: nil, node: nil, bindings: %{}
15 |
16 | @type t() :: %Retex.Token{}
17 |
18 | def new do
19 | %__MODULE__{}
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/wme.ex:
--------------------------------------------------------------------------------
1 | defmodule Retex.Wme do
2 | @moduledoc """
3 | A working memory element, it represent the world in the form of identifier, attribute and values
4 | timestamp is set at time of insertion into retex
5 | """
6 | @type wme_identifier() :: String.t() | atom()
7 | @type attribute() :: String.t() | atom()
8 | @type id() :: String.t() | number()
9 | @type value() :: any()
10 |
11 | @type t :: %__MODULE__{
12 | identifier: wme_identifier(),
13 | attribute: attribute(),
14 | id: id(),
15 | timestamp: number(),
16 | value: value()
17 | }
18 |
19 | defstruct identifier: nil, attribute: nil, value: nil, id: nil, timestamp: nil
20 | alias Retex.Fact.HasAttribute
21 |
22 | def new(id, attr, val) do
23 | item = %__MODULE__{identifier: id, attribute: attr, value: val}
24 | Map.put(item, :id, Retex.hash(item))
25 | end
26 |
27 | def to_has_attribute(%Retex.Wme{} = wme) do
28 | %__MODULE__{identifier: id, attribute: attr, value: val} = wme
29 | HasAttribute.new(owner: id, attribute: attr, predicate: :==, value: val)
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/agenda/execute_once.ex:
--------------------------------------------------------------------------------
1 | defmodule Retex.Agenda.ExecuteOnce do
2 | @moduledoc false
3 | @behaviour Retex.Agenda.Strategy
4 |
5 | def consume_agenda(executed_rules, %{agenda: [rule | rest] = _agenda} = network) do
6 | {rule_id, network} = execute_rule(rule, network)
7 |
8 | executed_rules = [rule_id] ++ executed_rules
9 |
10 | next_rules =
11 | network.agenda
12 | |> Enum.reject(fn pnode ->
13 | Enum.member?(executed_rules, pnode.id)
14 | end)
15 |
16 | network =
17 | if Enum.empty?(next_rules) do
18 | network
19 | else
20 | consume_agenda(executed_rules, %{network | agenda: next_rules ++ rest})
21 | end
22 |
23 | {executed_rules, network}
24 | end
25 |
26 | def execute_rule(%{id: id, raw_action: raw_action} = _rule, network) do
27 | new_network =
28 | Enum.reduce(raw_action, network, fn action, network ->
29 | do_execute_action(action, network)
30 | end)
31 |
32 | {id, new_network}
33 | end
34 |
35 | defp do_execute_action(%Retex.Wme{} = wme, network) do
36 | Retex.add_wme(network, wme)
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/config/config.exs:
--------------------------------------------------------------------------------
1 | # This file is responsible for configuring your application
2 | # and its dependencies with the aid of the Mix.Config module.
3 | import Config
4 |
5 | # This configuration is loaded before any dependency and is restricted
6 | # to this project. If another project depends on this project, this
7 | # file won't be loaded nor affect the parent project. For this reason,
8 | # if you want to provide default values for your application for
9 | # third-party users, it should be done in your "mix.exs" file.
10 |
11 | # You can configure your application as:
12 | #
13 | # config :retex, key: :value
14 | #
15 | # and access this configuration in your application as:
16 | #
17 | # Application.get_env(:retex, :key)
18 | #
19 | # You can also configure a third-party app:
20 | #
21 | # config :logger, level: :info
22 | #
23 |
24 | # It is also possible to import configuration files, relative to this
25 | # directory. For example, you can emulate configuration per environment
26 | # by uncommenting the line below and defining dev.exs, test.exs and such.
27 | # Configuration from the imported file will override the ones defined
28 | # here (which is why it is important to import them last).
29 | #
30 | # import_config "#{Mix.env()}.exs"
31 |
--------------------------------------------------------------------------------
/lib/facts/facts.ex:
--------------------------------------------------------------------------------
1 | defmodule Retex.Facts do
2 | @moduledoc false
3 | alias Retex.Fact
4 |
5 | @type t :: Fact.Relation.t() | Fact.Isa.t() | Fact.HasAttribute.t()
6 |
7 | def relation(from, name, to) do
8 | Fact.Relation.new(name: name, from: from, to: to)
9 | end
10 |
11 | def isa(variable, type) do
12 | isa(variable: variable, type: type)
13 | end
14 |
15 | def is_not(variable, type) do
16 | is_not(variable: variable, type: type)
17 | end
18 |
19 | def filter(variable, predicate, value) do
20 | Fact.Filter.new(variable: variable, predicate: predicate, value: value)
21 | end
22 |
23 | def isa(fields) do
24 | Fact.Isa.new(fields)
25 | end
26 |
27 | def is_not(fields) do
28 | Fact.IsNot.new(fields)
29 | end
30 |
31 | def has_attribute(owner, attribute, predicate, value) do
32 | has_attribute(owner: owner, attribute: attribute, predicate: predicate, value: value)
33 | end
34 |
35 | def has_attribute(fields) do
36 | Fact.HasAttribute.new(fields)
37 | end
38 |
39 | def not_existing_attribute(owner, attribute) do
40 | not_existing_attribute(owner: owner, attribute: attribute)
41 | end
42 |
43 | def not_existing_attribute(fields) do
44 | Fact.NotExistingAttribute.new(fields)
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/lib/facts/not_existing_attribute.ex:
--------------------------------------------------------------------------------
1 | defmodule Retex.Fact.NotExistingAttribute do
2 | @moduledoc "Attribute values that a Wme should NOT have in order for this condition to be true"
3 |
4 | defstruct owner: nil, attribute: nil
5 |
6 | @type owner :: String.t() | atom()
7 | @type attribute :: String.t() | atom()
8 | @type fields :: [owner: owner(), attribute: attribute()]
9 |
10 | @type t :: %Retex.Fact.NotExistingAttribute{
11 | owner: owner(),
12 | attribute: attribute()
13 | }
14 | @spec new(fields()) :: t()
15 | def new(fields) do
16 | struct(__MODULE__, fields)
17 | end
18 |
19 | defimpl Retex.Protocol.AlphaNetwork do
20 | alias Retex.{Fact, Node}
21 |
22 | def append(%Fact.NotExistingAttribute{} = condition, {graph, nodes}) do
23 | %{attribute: attribute, owner: class} = condition
24 | type_node = Node.Type.new(class)
25 | negative_select_node = Node.SelectNot.new(class, attribute)
26 |
27 | new_graph =
28 | graph
29 | |> Graph.add_vertex(type_node)
30 | |> Graph.add_edge(Retex.root_vertex(), type_node)
31 | |> Graph.add_vertex(negative_select_node)
32 | |> Graph.add_edge(type_node, negative_select_node)
33 |
34 | {new_graph, [negative_select_node | nodes]}
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/lib/nodes/type_select_not.ex:
--------------------------------------------------------------------------------
1 | defmodule Retex.Node.SelectNot do
2 | @moduledoc """
3 | The select nodes are checking for attributes, if they do NOT exists and are linked to the
4 | right owner from above, they will be activated and pass the tokens down
5 | """
6 | defstruct class: nil, id: nil, bindings: %{}, parent: nil
7 | @type t() :: %Retex.Node.SelectNot{}
8 |
9 | def new(parent, class) do
10 | item = %__MODULE__{class: class, parent: parent}
11 | %{item | id: Retex.hash(item)}
12 | end
13 |
14 | defimpl Retex.Protocol.Activation do
15 | def activate(
16 | %Retex.Node.SelectNot{class: attribute} = neighbor,
17 | %Retex{} = rete,
18 | %Retex.Wme{attribute: attribute} = wme,
19 | bindings,
20 | tokens
21 | ) do
22 | rete
23 | |> Retex.create_activation(neighbor, wme)
24 | |> Retex.add_token(neighbor, wme, bindings, tokens)
25 | |> Retex.deactivate_descendants(neighbor)
26 | |> Retex.stop_traversal(%{})
27 | end
28 |
29 | def activate(
30 | %Retex.Node.SelectNot{class: _class} = _neighbor,
31 | %Retex{} = rete,
32 | %Retex.Wme{attribute: _attribute},
33 | bindings,
34 | _tokens
35 | ) do
36 | Retex.stop_traversal(rete, bindings)
37 | end
38 |
39 | @spec active?(%{id: any}, Retex.t()) :: boolean()
40 | def active?(%{id: id}, %Retex{activations: activations}) do
41 | Enum.empty?(Map.get(activations, id, []))
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/lib/nodes/negative_type_node.ex:
--------------------------------------------------------------------------------
1 | defmodule Retex.Node.NegativeType do
2 | @moduledoc """
3 | The NegativeNodeType if part of the alpha network, the discrimination part of the network
4 | that check if a specific class DOES NOT exist. If this is the case, it propagates the activations
5 | down to the select node types. They will select an attribute and check for its test to pass.
6 | """
7 | defstruct class: nil, id: nil
8 | @type t :: %Retex.Node.Type{}
9 |
10 | def new(class) do
11 | item = %__MODULE__{class: class}
12 | %{item | id: Retex.hash(item)}
13 | end
14 |
15 | defimpl Retex.Protocol.Activation do
16 | def activate(
17 | %Retex.Node.NegativeType{class: identifier} = neighbor,
18 | %Retex{} = rete,
19 | %Retex.Wme{identifier: identifier} = wme,
20 | bindings,
21 | tokens
22 | ) do
23 | rete
24 | |> Retex.create_activation(neighbor, wme)
25 | |> Retex.add_token(neighbor, wme, bindings, tokens)
26 | |> Retex.deactivate_descendants(neighbor)
27 | |> Retex.stop_traversal(%{})
28 | end
29 |
30 | def activate(
31 | %Retex.Node.NegativeType{class: _class},
32 | %Retex{graph: _graph} = rete,
33 | %Retex.Wme{identifier: _identifier} = _wme,
34 | _bindings,
35 | _tokens
36 | ) do
37 | Retex.stop_traversal(rete, %{})
38 | end
39 |
40 | @spec active?(%{id: any}, Retex.t()) :: boolean()
41 | def active?(%{id: id}, %Retex{activations: activations}) do
42 | Enum.empty?(Map.get(activations, id, []))
43 | end
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/lib/facts/has_attribute.ex:
--------------------------------------------------------------------------------
1 | defmodule Retex.Fact.HasAttribute do
2 | @moduledoc "Attribute values that a Wme should have in order for this condition to be true"
3 |
4 | defstruct owner: nil, attribute: nil, predicate: nil, value: nil
5 |
6 | @type owner :: String.t() | atom()
7 | @type attribute :: String.t() | atom()
8 | @type predicate :: :== | :=== | :!== | :!= | :> | :< | :<= | :>= | :in
9 | @type value :: any()
10 | @type fields :: [owner: owner(), attribute: attribute(), predicate: predicate(), value: value()]
11 |
12 | @type t :: %Retex.Fact.HasAttribute{
13 | owner: owner(),
14 | attribute: attribute(),
15 | predicate: predicate(),
16 | value: value()
17 | }
18 | @spec new(fields()) :: t()
19 | def new(fields) do
20 | struct(__MODULE__, fields)
21 | end
22 |
23 | defimpl Retex.Protocol.AlphaNetwork do
24 | alias Retex.{Fact.HasAttribute, Node}
25 |
26 | def append(%HasAttribute{} = condition, {graph, test_nodes}) do
27 | %{attribute: attribute, owner: class, predicate: predicate, value: value} = condition
28 | condition_id = Retex.hash(condition)
29 | type_node = Node.Type.new(class)
30 | select_node = Node.Select.new(class, attribute)
31 | test_node = Node.Test.new([predicate, value], condition_id)
32 |
33 | new_graph =
34 | graph
35 | |> Graph.add_vertex(type_node)
36 | |> Graph.add_edge(Retex.root_vertex(), type_node)
37 | |> Graph.add_vertex(select_node)
38 | |> Graph.add_edge(type_node, select_node)
39 | |> Graph.add_vertex(test_node)
40 | |> Graph.add_edge(select_node, test_node)
41 |
42 | {new_graph, [test_node | test_nodes]}
43 | end
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/lib/nodes/type_select.ex:
--------------------------------------------------------------------------------
1 | defmodule Retex.Node.Select do
2 | @moduledoc """
3 | The select nodes are checking for attributes, if they exists and are linked to the
4 | right owner from above, they will be activated and pass the tokens down in the test
5 | nodes (that will check for their value instead).
6 | """
7 | defstruct class: nil, id: nil, parent: nil
8 | @type t() :: %Retex.Node.Select{}
9 |
10 | def new(parent, class) do
11 | item = %__MODULE__{class: class, parent: parent}
12 | %{item | id: Retex.hash(item)}
13 | end
14 |
15 | defimpl Retex.Protocol.Activation do
16 | def activate(
17 | %Retex.Node.Select{class: "$" <> variable} = neighbor,
18 | %Retex{} = rete,
19 | %Retex.Wme{attribute: attribute} = wme,
20 | bindings,
21 | tokens
22 | ) do
23 | rete
24 | |> Retex.create_activation(neighbor, wme)
25 | |> Retex.add_token(neighbor, wme, Map.merge(bindings, %{variable => attribute}), tokens)
26 | |> Retex.continue_traversal(Map.merge(bindings, %{variable => attribute}), neighbor, wme)
27 | end
28 |
29 | def activate(
30 | %Retex.Node.Select{class: attribute} = neighbor,
31 | %Retex{} = rete,
32 | %Retex.Wme{attribute: attribute} = wme,
33 | bindings,
34 | tokens
35 | ) do
36 | rete
37 | |> Retex.create_activation(neighbor, wme)
38 | |> Retex.add_token(neighbor, wme, bindings, tokens)
39 | |> Retex.continue_traversal(bindings, neighbor, wme)
40 | end
41 |
42 | def activate(
43 | %Retex.Node.Select{class: _class} = _neighbor,
44 | %Retex{} = rete,
45 | %Retex.Wme{attribute: _attribute},
46 | bindings,
47 | _tokens
48 | ) do
49 | Retex.stop_traversal(rete, bindings)
50 | end
51 |
52 | @spec active?(%{id: any}, Retex.t()) :: boolean()
53 | def active?(%{id: id}, %Retex{activations: activations}) do
54 | Enum.any?(Map.get(activations, id, []))
55 | end
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/lib/nodes/type_test.ex:
--------------------------------------------------------------------------------
1 | defmodule Retex.Node.Test do
2 | @moduledoc """
3 | Test node.
4 |
5 | If we reached that node, means that we are checking that a value is matching
6 | a specific condition. If this is the case, we activate this node and pass the token down
7 | to the beta network.
8 | """
9 | defstruct class: nil, id: nil
10 | @type t :: %Retex.Node.Test{}
11 |
12 | def new(class, id), do: %__MODULE__{class: class, id: id}
13 |
14 | defimpl Retex.Protocol.Activation do
15 | def activate(
16 | %Retex.Node.Test{class: [_operator, "$" <> _variable = var]} = neighbor,
17 | %Retex{activations: _activations} = rete,
18 | %{value: value} = wme,
19 | bindings,
20 | tokens
21 | ) do
22 | rete
23 | |> Retex.create_activation(neighbor, wme)
24 | |> Retex.add_token(neighbor, wme, Map.merge(bindings, %{var => value}), tokens)
25 | |> Retex.continue_traversal(Map.merge(bindings, %{var => value}), neighbor, wme)
26 | end
27 |
28 | def activate(
29 | %Retex.Node.Test{class: [:in, value]} = neighbor,
30 | %Retex{activations: _activations} = rete,
31 | wme,
32 | bindings,
33 | tokens
34 | ) do
35 | if Enum.member?(value, wme.value) do
36 | rete
37 | |> Retex.create_activation(neighbor, wme)
38 | |> Retex.add_token(neighbor, wme, bindings, tokens)
39 | |> Retex.continue_traversal(bindings, neighbor, wme)
40 | else
41 | Retex.stop_traversal(rete, %{})
42 | end
43 | end
44 |
45 | def activate(
46 | %Retex.Node.Test{class: [operator, value]} = neighbor,
47 | %Retex{activations: _activations} = rete,
48 | wme,
49 | bindings,
50 | tokens
51 | ) do
52 | if apply(Kernel, operator, [wme.value, value]) do
53 | rete
54 | |> Retex.create_activation(neighbor, wme)
55 | |> Retex.add_token(neighbor, wme, bindings, tokens)
56 | |> Retex.continue_traversal(bindings, neighbor, wme)
57 | else
58 | Retex.stop_traversal(rete, %{})
59 | end
60 | end
61 |
62 | @spec active?(%{id: any}, Retex.t()) :: boolean()
63 | def active?(%{id: id}, %Retex{activations: activations}) do
64 | Enum.any?(Map.get(activations, id, []))
65 | end
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/benchmark/rule_chain.exs:
--------------------------------------------------------------------------------
1 | defmodule Benchmark do
2 | alias Retex.Agenda
3 |
4 | defp has_attribute(owner, attribute, predicate, value) do
5 | has_attribute(owner: owner, attribute: attribute, predicate: predicate, value: value)
6 | end
7 |
8 | defp has_attribute(fields) do
9 | Retex.Fact.HasAttribute.new(fields)
10 | end
11 |
12 | defp create_rule(lhs: given, rhs: action, id: id) do
13 | %{
14 | given: given,
15 | id: id,
16 | then: action
17 | }
18 | end
19 |
20 | def generate_rule_chain(depth) do
21 | for level <- 1..depth do
22 | given = [
23 | has_attribute("Thing_#{level}", "attribute_#{level}", :==, level),
24 | has_attribute("Thing_#{level}", "attribute_#{level}", :!=, false)
25 | ]
26 |
27 | then =
28 | if level + 1 >= depth do
29 | []
30 | else
31 | [Retex.Wme.new("Thing_#{level + 1}", "attribute_#{level + 1}", level + 1)]
32 | end
33 |
34 | create_rule(lhs: given, rhs: then, id: depth)
35 | end
36 | end
37 |
38 | def run(depth \\ 20000) do
39 | depth =
40 | case parse(System.argv()) do
41 | {[depth: depth], _, _} -> depth
42 | _ -> depth
43 | end
44 |
45 | rules = generate_rule_chain(depth)
46 | require Logger
47 |
48 | Logger.info("Adding #{depth} rules...")
49 |
50 | {time, rete_engine} =
51 | :timer.tc(fn ->
52 | Enum.reduce(rules, Retex.new(), fn rule, network ->
53 | Retex.add_production(network, rule)
54 | end)
55 | end)
56 |
57 | Graph.info(rete_engine.graph) |> inspect() |> Logger.info()
58 | Logger.info("Adding #{depth} rules took #{humanized_duration(time)}")
59 |
60 | wme = Retex.Wme.new("Thing_1", "attribute_1", 1)
61 |
62 | {time, rete_engine} = :timer.tc(fn -> Retex.add_wme(rete_engine, wme) end)
63 |
64 | Agenda.ExecuteOnce.consume_agenda([], rete_engine)
65 |
66 | Logger.info("Adding the working memory took #{humanized_duration(time)}")
67 | end
68 |
69 | defp parse(args) do
70 | OptionParser.parse(args, strict: [depth: :integer])
71 | end
72 |
73 | defp humanized_duration(time) do
74 | duration_in_seconds = time / 1_000_000
75 |
76 | duration_in_seconds
77 | |> Timex.Duration.from_seconds()
78 | |> Timex.Format.Duration.Formatter.format(:humanized)
79 | end
80 | end
81 |
82 | Benchmark.run()
83 |
--------------------------------------------------------------------------------
/lib/nodes/p_node.ex:
--------------------------------------------------------------------------------
1 | defmodule Retex.Node.PNode do
2 | @moduledoc """
3 | Production node. This is like a production node in Rete algorithm. It is activated if all
4 | the conditions in a rule are matching and contains the action that can be executed as consequence.
5 | """
6 |
7 | defstruct action: nil, id: nil, raw_action: nil, bindings: %{}, filters: []
8 |
9 | @type t() :: %__MODULE__{}
10 |
11 | def new(action, filters \\ []) do
12 | item = %__MODULE__{action: action, raw_action: action, filters: filters}
13 | %{item | id: Retex.hash(item)}
14 | end
15 |
16 | defimpl Retex.Protocol.Activation do
17 | def activate(
18 | %{filters: filters} = neighbor,
19 | %Retex{tokens: tokens, graph: graph, activations: activations} = rete,
20 | _wme,
21 | _bindings,
22 | _tokens
23 | ) do
24 | [parent] =
25 | parents =
26 | Graph.in_neighbors(graph, neighbor)
27 | |> Enum.filter(fn node ->
28 | Map.get(activations, node.id)
29 | end)
30 |
31 | tokens = Map.get(tokens, parent.id)
32 |
33 | if Enum.all?(parents, &Map.get(activations, &1.id)) do
34 | bindings = tokens |> Enum.reduce(%{}, fn t, acc -> Map.merge(acc, t.bindings) end)
35 | productions = apply_filters([Retex.replace_bindings(neighbor, bindings)], filters)
36 |
37 | new_rete = %{
38 | rete
39 | | agenda: [productions | rete.agenda] |> List.flatten() |> Enum.uniq()
40 | }
41 |
42 | Retex.stop_traversal(new_rete, %{})
43 | else
44 | Retex.stop_traversal(rete, %{})
45 | end
46 | end
47 |
48 | def active?(_, _) do
49 | raise "Not implemented"
50 | end
51 |
52 | def apply_filters(nodes, filters) do
53 | Enum.filter(nodes, fn node -> pass_test?(filters, node) end)
54 | end
55 |
56 | defp pass_test?(filters, node) do
57 | Enum.reduce_while(filters, true, fn filter, _ ->
58 | if test_pass?(node, filter), do: {:cont, true}, else: {:halt, false}
59 | end)
60 | end
61 |
62 | def test_pass?(%Retex.Node.PNode{bindings: bindings}, %Retex.Fact.Filter{
63 | predicate: predicate,
64 | value: value,
65 | variable: variable
66 | }) do
67 | case Map.get(bindings, variable, :_undefined) do
68 | :_undefined -> true
69 | current_value -> apply(Kernel, predicate, [current_value, value])
70 | end
71 | end
72 | end
73 | end
74 |
--------------------------------------------------------------------------------
/lib/serializer.ex:
--------------------------------------------------------------------------------
1 | defmodule Retex.Serializer do
2 | @moduledoc """
3 | This serializer converts a `Graph` to a [Mermaid Flowchart](https://mermaid.js.org/syntax/flowchart.html).
4 | """
5 |
6 | use Graph.Serializer
7 | import Graph.Serializer
8 |
9 | @impl Graph.Serializer
10 | def serialize(%Graph{} = g) do
11 | result = """
12 | flowchart
13 | #{serialize_vertices(g)}
14 | #{serialize_edges(g)}
15 | """
16 |
17 | {:ok, result}
18 | end
19 |
20 | def vertex_labels(%Graph{vertex_labels: vl}, id, v) do
21 | case Map.get(vl, id) do
22 | [] -> encode(v)
23 | label -> encode(label)
24 | end
25 | end
26 |
27 | defp serialize_vertices(g) do
28 | Enum.map_join(g.vertices, "\n", fn {id, value} ->
29 | indent(1) <> "#{id}" <> "[\"" <> vertex_labels(g, id, value) <> "\"]"
30 | end)
31 | end
32 |
33 | defp serialize_edges(g) do
34 | arrow =
35 | case g.type do
36 | :directed -> "->"
37 | :undirected -> "-"
38 | end
39 |
40 | edges =
41 | g.vertices
42 | |> Enum.reduce([], fn {id, _}, acc ->
43 | out_edges =
44 | g.out_edges
45 | |> Map.get_lazy(id, &MapSet.new/0)
46 | |> Enum.flat_map(&fetch_edge(g, id, &1))
47 |
48 | acc ++ out_edges
49 | end)
50 |
51 | Enum.map_join(edges, "\n", &serialize_edge(&1, arrow))
52 | end
53 |
54 | defp fetch_edge(g, id, out_edge_id) do
55 | g.edges
56 | |> Map.fetch!({id, out_edge_id})
57 | |> Enum.map(fn
58 | {nil, weight} -> {id, out_edge_id, weight}
59 | {label, weight} -> {id, out_edge_id, weight, encode(label)}
60 | end)
61 | end
62 |
63 | defp encode(%{class: name}) when is_list(name) do
64 | Enum.join(name)
65 | end
66 |
67 | defp encode(%{class: name}) do
68 | to_string(name)
69 | end
70 |
71 | defp encode(%Retex.Node.BetaMemory{}) do
72 | "Join"
73 | end
74 |
75 | defp encode(%Retex.Node.PNode{id: _id, action: action}) do
76 | "#{inspect(action, pretty: true)}"
77 | end
78 |
79 | defp serialize_edge({id, out_edge_id, weight, label}, arrow) do
80 | indent(1) <> "#{id} " <> weight_arrow(arrow, weight) <> " |#{label}| " <> "#{out_edge_id}"
81 | end
82 |
83 | defp serialize_edge({id, out_edge_id, weight}, arrow) do
84 | indent(1) <> "#{id} " <> weight_arrow(arrow, weight) <> " #{out_edge_id}"
85 | end
86 |
87 | defp weight_arrow(arrow, weight) do
88 | String.duplicate("-", weight) <> arrow
89 | end
90 | end
91 |
--------------------------------------------------------------------------------
/lib/nodes/type_node.ex:
--------------------------------------------------------------------------------
1 | defmodule Retex.Node.Type do
2 | @moduledoc """
3 | The NodeType if part of the alpha network, the discrimination part of the network
4 | that check if a specific class exists. If this is the case, it propagates the activations
5 | down to the select node types. They will select an attribute and check for its existance.
6 | """
7 | defstruct class: nil, id: nil
8 | @type t :: %Retex.Node.Type{}
9 |
10 | def new(class) do
11 | item = %__MODULE__{class: class}
12 | %{item | id: Retex.hash(item)}
13 | end
14 |
15 | defimpl Retex.Protocol.Activation do
16 | def activate(
17 | %Retex.Node.Type{class: class} = neighbor,
18 | %Retex{graph: _graph} = rete,
19 | %Retex.Wme{identifier: "$" <> _identifier = var} = wme,
20 | bindings,
21 | tokens
22 | ) do
23 | new_bindings = Map.merge(bindings, %{var => class})
24 |
25 | rete
26 | |> Retex.create_activation(neighbor, wme)
27 | |> Retex.add_token(neighbor, wme, new_bindings, tokens)
28 | |> Retex.continue_traversal(new_bindings, neighbor, wme)
29 | end
30 |
31 | def activate(
32 | %Retex.Node.Type{class: "$" <> _variable = var} = neighbor,
33 | %Retex{graph: _graph} = rete,
34 | %Retex.Wme{identifier: identifier} = wme,
35 | bindings,
36 | tokens
37 | ) do
38 | rete
39 | |> Retex.create_activation(neighbor, wme)
40 | |> Retex.add_token(neighbor, wme, Map.merge(bindings, %{var => identifier}), tokens)
41 | |> Retex.continue_traversal(Map.merge(bindings, %{var => identifier}), neighbor, wme)
42 | end
43 |
44 | def activate(
45 | %Retex.Node.Type{class: identifier} = neighbor,
46 | %Retex{} = rete,
47 | %Retex.Wme{identifier: identifier} = wme,
48 | bindings,
49 | tokens
50 | ) do
51 | rete
52 | |> Retex.create_activation(neighbor, wme)
53 | |> Retex.add_token(neighbor, wme, bindings, tokens)
54 | |> Retex.continue_traversal(bindings, neighbor, wme)
55 | end
56 |
57 | def activate(
58 | %Retex.Node.Type{class: _class},
59 | %Retex{graph: _graph} = rete,
60 | %Retex.Wme{identifier: _identifier} = _wme,
61 | _bindings,
62 | _tokens
63 | ) do
64 | Retex.stop_traversal(rete, %{})
65 | end
66 |
67 | @spec active?(%{id: any}, Retex.t()) :: boolean()
68 | def active?(%{id: id}, %Retex{activations: activations}) do
69 | Enum.any?(Map.get(activations, id, []))
70 | end
71 | end
72 | end
73 |
--------------------------------------------------------------------------------
/benchmark/example.exs:
--------------------------------------------------------------------------------
1 | defmodule Benchmark do
2 | defp isa(variable, type) do
3 | isa(variable: variable, type: type)
4 | end
5 |
6 | defp isa(fields) do
7 | Retex.Fact.Isa.new(fields)
8 | end
9 |
10 | defp has_attribute(owner, attribute, predicate, value) do
11 | has_attribute(owner: owner, attribute: attribute, predicate: predicate, value: value)
12 | end
13 |
14 | defp has_attribute(fields) do
15 | Retex.Fact.HasAttribute.new(fields)
16 | end
17 |
18 | defp create_rule(lhs: given, rhs: action) do
19 | %{
20 | given: given,
21 | then: action
22 | }
23 | end
24 |
25 | defp rule(n, type \\ :Thing) do
26 | given = [
27 | has_attribute(:Thing, :status, :==, "$a_#{n}"),
28 | has_attribute(:Thing, :premium, :==, true)
29 | ]
30 |
31 | action = [
32 | {"$thing_#{n}", :account_status, "$a_#{n}"}
33 | ]
34 |
35 | rule = create_rule(lhs: given, rhs: action)
36 | end
37 |
38 | def run() do
39 | wme = Retex.Wme.new(:Account, :status, :silver)
40 | wme_2 = Retex.Wme.new(:Account, :premium, true)
41 | wme_3 = Retex.Wme.new(:Family, :size, 10)
42 |
43 | wme_4 = Retex.Wme.new(:Account, :status, :silver)
44 | wme_5 = Retex.Wme.new(:AccountC, :status, :silver)
45 | wme_5 = Retex.Wme.new(:AccountD, :status, :silver)
46 | number_of_rules = 100_000
47 |
48 | require Logger
49 |
50 | Logger.info("Adding #{number_of_rules} rules...")
51 |
52 | result =
53 | Timer.tc(fn ->
54 | Enum.reduce(1..number_of_rules, Retex.new(), fn n, network ->
55 | Retex.add_production(network, rule(n))
56 | end)
57 | end)
58 |
59 | duration = result[:humanized_duration]
60 | rules_100_000 = result[:reply]
61 | Logger.info("Adding #{number_of_rules} rules took #{duration}")
62 |
63 | given_matching = [
64 | has_attribute(:Account, :status, :==, "$account"),
65 | has_attribute(:Account, :premium, :==, true)
66 | ]
67 |
68 | action_2 = [
69 | {"$thing", :account_status, "$account"}
70 | ]
71 |
72 | rule = create_rule(lhs: given_matching, rhs: action_2)
73 | network = Retex.add_production(rules_100_000, rule)
74 |
75 | Logger.info("Network info #{Graph.info(rules_100_000.graph) |> inspect()}")
76 |
77 | result =
78 | Timer.tc(fn ->
79 | network
80 | |> Retex.add_wme(wme)
81 | |> Retex.add_wme(wme_2)
82 | |> Retex.add_wme(wme_3)
83 | |> Retex.add_wme(wme_4)
84 | |> Retex.add_wme(wme_5)
85 | end)
86 |
87 | duration = result[:humanized_duration]
88 | network = result[:reply]
89 |
90 | Logger.info(
91 | "Adding 6 working memories took #{duration} and matched #{Enum.count(network.agenda)} rules"
92 | )
93 |
94 | IO.inspect(agenda: network.agenda)
95 | end
96 | end
97 |
98 | Benchmark.run()
99 |
--------------------------------------------------------------------------------
/lib/nodes/beta_memory.ex:
--------------------------------------------------------------------------------
1 | defmodule Retex.Node.BetaMemory do
2 | @moduledoc """
3 | A BetaMemory works like a two input node in Rete. It is simply a join node
4 | between two tests that have passed successfully. The activation of a BetaMemory
5 | happens if the two parents (left and right) have been activated and the bindings
6 | are matching for both of them.
7 | """
8 | defstruct id: nil
9 | @type t :: %Retex.Node.BetaMemory{}
10 |
11 | def new(id) do
12 | %__MODULE__{id: id}
13 | end
14 |
15 | defimpl Retex.Protocol.Activation do
16 | alias Retex.Protocol.Activation
17 |
18 | def activate(neighbor, rete, wme, bindings, _tokens) do
19 | [left, right] = Graph.in_neighbors(rete.graph, neighbor)
20 |
21 | with true <- Activation.active?(left, rete),
22 | true <- Activation.active?(right, rete),
23 | left_tokens <- Map.get(rete.tokens, left.id),
24 | right_tokens <- Map.get(rete.tokens, right.id),
25 | new_tokens <- matching_tokens(neighbor, wme, right_tokens, left_tokens),
26 | true <- Enum.any?(new_tokens) do
27 | rete
28 | |> Retex.create_activation(neighbor, wme)
29 | |> Retex.add_token(neighbor, wme, bindings, new_tokens)
30 | |> Retex.continue_traversal(bindings, neighbor, wme)
31 | else
32 | _anything ->
33 | Retex.stop_traversal(rete, %{})
34 | end
35 | end
36 |
37 | defp matching_tokens(_, _, left, nil), do: left
38 | defp matching_tokens(_, _, nil, right), do: right
39 |
40 | defp matching_tokens(node, wme, left, right) do
41 | for %{bindings: left_bindings} <- left, %{bindings: right_bindings} <- right do
42 | if variables_match?(left_bindings, right_bindings) do
43 | [
44 | %{
45 | Retex.Token.new()
46 | | wmem: wme,
47 | node: node.id,
48 | bindings: Map.merge(left_bindings, right_bindings)
49 | }
50 | ]
51 | else
52 | []
53 | end
54 | end
55 | |> List.flatten()
56 | end
57 |
58 | defp variables_match?(left, right) do
59 | left_bindings_match_in_right_bindings?(left, right) &&
60 | right_bindings_match_in_left_bindings?(right, left)
61 | end
62 |
63 | defp left_bindings_match_in_right_bindings?(left, right) do
64 | Enum.reduce_while(left, true, fn {key, left_value}, true ->
65 | if Map.get(right, key, left_value) == left_value,
66 | do: {:cont, true},
67 | else: {:halt, false}
68 | end)
69 | end
70 |
71 | defp right_bindings_match_in_left_bindings?(right, left) do
72 | Enum.reduce_while(right, true, fn {key, right_value}, true ->
73 | if Map.get(left, key, right_value) == right_value,
74 | do: {:cont, true},
75 | else: {:halt, false}
76 | end)
77 | end
78 |
79 | @spec active?(%{id: any}, Retex.t()) :: boolean()
80 | def active?(%{id: id}, %Retex{activations: activations}) do
81 | Enum.any?(Map.get(activations, id, []))
82 | end
83 | end
84 | end
85 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule Retex.MixProject do
2 | use Mix.Project
3 | @source_url "https://github.com/lorenzosinisi/retex"
4 | @version "0.1.11"
5 |
6 | def project do
7 | [
8 | app: :retex,
9 | version: @version,
10 | elixir: "~> 1.12",
11 | aliases: aliases(),
12 | start_permanent: Mix.env() == :prod,
13 | source_url: "https://github.com/lorenzosinisi/retex",
14 | description: description(),
15 | package: package(),
16 | deps: deps(),
17 | docs: docs(),
18 | test_coverage: [tool: ExCoveralls],
19 | preferred_cli_env: [
20 | coveralls: :test,
21 | "coveralls.detail": :test,
22 | "coveralls.post": :test,
23 | "coveralls.html": :test
24 | ],
25 | dialyzer: [
26 | plt_file: {:no_warn, "priv/plts/dialyzer.plt"},
27 | ignore_warnings: ".dialyzer_ignore.exs"
28 | ]
29 | ]
30 | end
31 |
32 | defp description do
33 | """
34 | Rete algorithm in Elixir
35 | """
36 | end
37 |
38 | # Run "mix help compile.app" to learn about applications.
39 | def application do
40 | [
41 | extra_applications: [:logger]
42 | ]
43 | end
44 |
45 | defp package do
46 | [
47 | files: ["lib", "mix.exs", "README.md", "LICENSE"],
48 | maintainers: ["Lorenzo Sinisi"],
49 | licenses: ["Apache 2.0"],
50 | links: %{"GitHub" => "https://github.com/lorenzosinisi/retex"}
51 | ]
52 | end
53 |
54 | # Run "mix help deps" to learn about dependencies.
55 | defp deps do
56 | [
57 | {:libgraph, "~> 0.16.0"},
58 | {:uuid_tools, "~> 0.1.0"},
59 | {:duration_tc, "~> 0.1.1", only: :dev},
60 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false},
61 | {:dialyxir, "~> 1.3", only: [:dev], runtime: false},
62 | {:excoveralls, "~> 0.10", only: :test},
63 | {:credo, "~> 1.7", only: [:dev, :test], runtime: false},
64 | {:sobelow, "~> 0.7", only: [:dev, :test], runtime: false}
65 | ]
66 | end
67 |
68 | defp docs do
69 | [
70 | extras: [
71 | "CHANGELOG.md": [],
72 | LICENSE: [title: "License"],
73 | "README.md": [title: "Overview"]
74 | ],
75 | assets: "assets",
76 | main: "readme",
77 | canonical: "http://hexdocs.pm/retex",
78 | homepage_url: @source_url,
79 | source_url: @source_url,
80 | source_ref: "v#{@version}",
81 | before_closing_body_tag: &before_closing_body_tag/1
82 | ]
83 | end
84 |
85 | defp before_closing_body_tag(:html) do
86 | """
87 |
88 |
106 | """
107 | end
108 |
109 | defp before_closing_body_tag(_), do: ""
110 |
111 | defp aliases do
112 | [
113 | test: [
114 | "format",
115 | "coveralls",
116 | "credo",
117 | "sobelow --skip -i Config.HTTPS --verbose"
118 | ]
119 | ]
120 | end
121 | end
122 |
--------------------------------------------------------------------------------
/mix.lock:
--------------------------------------------------------------------------------
1 | %{
2 | "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
3 | "certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
4 | "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
5 | "credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"},
6 | "dialyxir": {:hex, :dialyxir, "1.3.0", "fd1672f0922b7648ff9ce7b1b26fcf0ef56dda964a459892ad15f6b4410b5284", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "00b2a4bcd6aa8db9dcb0b38c1225b7277dca9bc370b6438715667071a304696f"},
7 | "duration_tc": {:hex, :duration_tc, "0.1.1", "fa72b26fc8d97abb9492f2e50fc90306e1156125bd4f4c75aa60cd335e5658f1", [:mix], [{:timex, "~> 3.5", [hex: :timex, repo: "hexpm", optional: false]}], "hexpm", "d8ec390dba34f5164e085fddb3642353fe4f024ceb51653db042a26a4519956e"},
8 | "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"},
9 | "earmark_parser": {:hex, :earmark_parser, "1.4.33", "3c3fd9673bb5dcc9edc28dd90f50c87ce506d1f71b70e3de69aa8154bc695d44", [:mix], [], "hexpm", "2d526833729b59b9fdb85785078697c72ac5e5066350663e5be6a1182da61b8f"},
10 | "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
11 | "ex_doc": {:hex, :ex_doc, "0.29.4", "6257ecbb20c7396b1fe5accd55b7b0d23f44b6aa18017b415cb4c2b91d997729", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2c6699a737ae46cb61e4ed012af931b57b699643b24dabe2400a8168414bc4f5"},
12 | "excoveralls": {:hex, :excoveralls, "0.16.1", "0bd42ed05c7d2f4d180331a20113ec537be509da31fed5c8f7047ce59ee5a7c5", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "dae763468e2008cf7075a64cb1249c97cb4bc71e236c5c2b5e5cdf1cfa2bf138"},
13 | "expo": {:hex, :expo, "0.4.1", "1c61d18a5df197dfda38861673d392e642649a9cef7694d2f97a587b2cfb319b", [:mix], [], "hexpm", "2ff7ba7a798c8c543c12550fa0e2cbc81b95d4974c65855d8d15ba7b37a1ce47"},
14 | "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
15 | "gettext": {:hex, :gettext, "0.22.3", "c8273e78db4a0bb6fba7e9f0fd881112f349a3117f7f7c598fa18c66c888e524", [:mix], [{:expo, "~> 0.4.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "935f23447713954a6866f1bb28c3a878c4c011e802bcd68a726f5e558e4b64bd"},
16 | "graphvix": {:hex, :graphvix, "1.0.0", "e1c2e8f2950c0ff43e64f1d3788d095ed7481b29cccaf63f80b2fdf751c6cdd3", [:mix], [], "hexpm"},
17 | "hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~> 2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"},
18 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
19 | "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"},
20 | "libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"},
21 | "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"},
22 | "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"},
23 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"},
24 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
25 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
26 | "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"},
27 | "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
28 | "sanskrit": {:git, "https://github.com/lorenzosinisi/sanskrit", "a599df828a67daedd179cfa34518c254058941b0", []},
29 | "sobelow": {:hex, :sobelow, "0.12.2", "45f4d500e09f95fdb5a7b94c2838d6b26625828751d9f1127174055a78542cf5", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "2f0b617dce551db651145662b84c8da4f158e7abe049a76daaaae2282df01c5d"},
30 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
31 | "timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"},
32 | "tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"},
33 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
34 | "uuid_tools": {:hex, :uuid_tools, "0.1.1", "e94e05551dcfd085a127c11112b0e6d24a98963659bf2f16615745c492e00e38", [:mix], [], "hexpm", "eafa199917316fece589c8768427c0bac6d6959a523482d130f1997a674c0da4"},
35 | }
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/lib/retex.ex:
--------------------------------------------------------------------------------
1 | defmodule Retex do
2 | @moduledoc """
3 | The algorithm utilizes symbols to create an internal representation of the world.
4 | Each element in the real world is converted into a triple known as a "Working Memory Element" (`Retex.Wme.t()`),
5 | represented as {Entity, attribute, attribute_value}.
6 |
7 | The world is represented through facts (WMEs) and Rules.
8 | A Rule consists of two essential parts: the "given" (right side) and the "then" (left side).
9 |
10 | To perform inference, the rule generates a directed graph starting from a common and generic Root node,
11 | which branches out to form leaf nodes. The branches from the Root node correspond to the initial part
12 | of the WME, representing the working memory elements or "Entity". For instance, if we want to
13 | represent a customer's account status as "silver", we would encode it as "{Customer, account_status, silver}".
14 | Alternatively, with the use of a struct, we can achieve the same representation as `Retex.Wme.new("Customer", "account status", "silver")`
15 |
16 |
17 |
18 | ## The struct
19 | This module also defines a Struct with the following fields:
20 |
21 | 1. `graph: Graph.new()`: This field is initialized with the value returned by the `Graph.new()` function. It is a reference to a directed graph data structure (using `libgraph`)
22 | that represents a network of interconnected nodes and vertices.
23 |
24 | 2. `wmes: %{}`: This field is initialized as an empty map. It is expected to store "working memory elements" (WMEs), which are pieces of information or facts used in the system.
25 |
26 | 3. `agenda: []`: This field is initialized as an empty list. It is intended to store a collection of tasks or items that need to be processed or executed. The agenda typically represents a prioritized queue of pending actions.
27 |
28 | 4. `activations: %{}`: This field is initialized as an empty map. It is used to store information related to the activations of nodes in the network.
29 |
30 | 5. `wme_activations: %{}`: This field is initialized as an empty map. It is similar to the `activations` field but specifically focuses on the activations of working memory elements (WMEs) and can serve as reverse lookup of which facts have activated which no.
31 |
32 | 6. `tokens: MapSet.new()`: This field is initialized with the value returned by the `MapSet.new()` function. Will be deprecated soon because it has no use but was a porting from the original paper.
33 |
34 | 7. `bindings: %{}`: This field is initialized as an empty map. It is used to store variable bindings or associations between variables and their corresponding values. This can be useful for tracking and manipulating data within the system.
35 |
36 | 8. `pending_activation: []`: This field is initialized as an empty list. It is likely used to keep track of activations that are pending or awaiting further processing. The exact meaning and usage of these pending activations would depend on the system's design.
37 | """
38 |
39 | @type t() :: %Retex{}
40 | alias Retex.{Fact, Node, Protocol, Protocol.AlphaNetwork, Token}
41 |
42 | alias Node.{
43 | BetaMemory,
44 | PNode,
45 | Select,
46 | Test,
47 | Type
48 | }
49 |
50 | @type action :: %{given: list(Retex.Wme.t()), then: list(Retex.Wme.t())}
51 | @type network_node :: Type.t() | Test.t() | Select.t() | PNode.t() | BetaMemory.t()
52 |
53 | # the Retex network is made of the following elements:
54 | defstruct graph: Graph.new(),
55 | wmes: %{},
56 | agenda: [],
57 | activations: %{},
58 | wme_activations: %{},
59 | tokens: MapSet.new(),
60 | bindings: %{},
61 | pending_activation: []
62 |
63 | @doc """
64 | Generate a new Retext.Root.t() struct that represents the first node of the network.
65 | An anonymous node that functions just as connector for the type nodes (`Retex.Node.Type.t()`)
66 | """
67 | @spec root_vertex :: Retex.Root.t()
68 | def root_vertex, do: Retex.Root.new()
69 |
70 | @doc @moduledoc
71 | @spec new :: Retex.t()
72 | def new do
73 | %{graph: graph} = %Retex{}
74 | graph = Graph.add_vertex(graph, Retex.Root.new())
75 | %Retex{graph: graph}
76 | end
77 |
78 | @doc """
79 | Takes the network itself and a WME struct and tries to activate as many nodes as possible traversing the graph
80 | from the Root until each reachable branch executing a series of "tests" (pattern matching) at each node level.
81 |
82 | Each node is tested implementing the activation protocol, so to know if how the test for the node against the WME
83 | works check their protocol implementation.
84 | """
85 | @spec add_wme(Retex.t(), Retex.Wme.t()) :: Retex.t()
86 | def add_wme(%Retex{} = network, %Retex.Wme{} = wme) do
87 | # just a timestamp might be useful for the future
88 | wme = Map.put(wme, :timestamp, :os.system_time(:seconds))
89 | # store the new fact in memory with their ID
90 | network = %{network | wmes: Map.put(network.wmes, wme.id, wme)}
91 | # traverse the network for the Root node branching out and testing each node for possible
92 | # activations
93 | {network, bindings} = propagate_activations(network, root_vertex(), wme, network.bindings)
94 |
95 | # if there is some activation for variables store such mapping (variable name => value) in memory
96 | %{network | bindings: Map.merge(network.bindings, bindings)}
97 | end
98 |
99 | @doc """
100 | A production is what is called a Rule in the original Rete paper from C. Forgy
101 |
102 | A production is a map of given and then and each of those fields contains a list of
103 | `Retex.Fact.t()` which can be tested against a `Retex.Wme.t()`
104 | """
105 | @spec add_production(Retex.t(), %{given: list(Retex.Wme.t()), then: action()}) :: t()
106 | def add_production(%{graph: graph} = network, %{given: given, then: action}) do
107 | # Filters are extra tests that ca be added to the given part of the rule
108 | # it is an extra feature present only in Retex and a personal choice
109 | {filters, given} = Enum.split_with(given, &is_filter?/1)
110 | # Take each fact in the given part of the rule and find or create the corresponding node
111 | # this part of what you see in the graph at https://github.com/lorenzosinisi/retex#how-does-it-work
112 | {graph, alphas} = build_alpha_network(graph, given)
113 | # this second part is just the representation of each "and" in a rule.
114 | {beta_memory, graph} = build_beta_network(graph, alphas)
115 |
116 | # finally add the "then" part of the rule on the last leaf of that subgraph (generated by the Rule)
117 | graph = add_p_node(graph, beta_memory, action, filters)
118 |
119 | %{network | graph: graph}
120 | end
121 |
122 | defp is_filter?(%Fact.Filter{}), do: true
123 | defp is_filter?(_), do: false
124 |
125 | @doc """
126 | Take each fact of the "given" part of the rule and construct the alpha part of the network destructuring the facts
127 | into "Root" -> "Entity" -> "Attribute" -> "Value" (if a node with the same value already exists it will be a noop)
128 | """
129 | @spec build_alpha_network(Graph.t(), list()) :: {Graph.t(), list()}
130 | def build_alpha_network(graph, given) do
131 | Enum.reduce(given, {graph, []}, &AlphaNetwork.append(&1, &2))
132 | end
133 |
134 | @doc """
135 | After building the alpha network we will have a list of nodes which are the bottom of the new subnetwork,
136 | not connect those two by two. Take the firs two, join them with a new node, that that one node
137 | connect it with the next orphan node, keep doing it until all the facts of a rule are connected together and
138 | we have one last "Join" node.
139 |
140 | And example of a graph with only one "and/join" node:
141 |
142 | ```mermaid
143 | flowchart
144 | 2102090852["==100"]
145 | 2332826675["==silver"]
146 | 2833714732["[{:Discount, :code, 50}]"]
147 | 3108351631["Root"]
148 | 3726656564["Join"]
149 | 3801762854["miles"]
150 | 3860425667["Customer"]
151 | 3895425755["account_status"]
152 | 4112061991["Flight"]
153 | 2102090852 --> 3726656564
154 | 2332826675 --> 3726656564
155 | 3108351631 --> 3860425667
156 | 3108351631 --> 4112061991
157 | 3726656564 --> 2833714732
158 | 3801762854 --> 2102090852
159 | 3860425667 --> 3895425755
160 | 3895425755 --> 2332826675
161 | 4112061991 --> 3801762854
162 | ```
163 | """
164 | @spec build_beta_network(Graph.t(), list(network_node())) :: {list(network_node()), Graph.t()}
165 | def build_beta_network(graph, [first, second | list]) do
166 | id = Retex.hash(Enum.sort([first, second]))
167 | beta_memory = Node.BetaMemory.new(id)
168 |
169 | graph
170 | |> Graph.add_vertex(beta_memory)
171 | |> Graph.add_edge(first, beta_memory)
172 | |> Graph.add_edge(second, beta_memory)
173 | |> build_beta_network([beta_memory | list])
174 | end
175 |
176 | def build_beta_network(graph, [beta_memory]) do
177 | {beta_memory, graph}
178 | end
179 |
180 | @doc """
181 | The P node is the production node, just another name of a rule
182 | """
183 | @spec add_p_node(Graph.t(), BetaMemory.t(), action(), list(Fact.Filter.t())) :: Graph.t()
184 | def add_p_node(graph, beta_memory, action, filters) do
185 | pnode = Node.PNode.new(action, filters)
186 | graph |> Graph.add_vertex(pnode) |> Graph.add_edge(beta_memory, pnode)
187 | end
188 |
189 | @max_phash 4_294_967_296
190 | @spec hash(any) :: String.t()
191 | def hash(:uuid4), do: UUIDTools.uuid4()
192 |
193 | def hash(data) do
194 | :erlang.phash2(data, @max_phash)
195 | end
196 |
197 | @spec replace_bindings(PNode.t(), map) :: PNode.t()
198 | def replace_bindings(%_{action: action_fun} = pnode, bindings)
199 | when is_function(action_fun) do
200 | %{pnode | action: action_fun, bindings: bindings}
201 | end
202 |
203 | def replace_bindings(%_{action: actions} = pnode, bindings)
204 | when is_list(actions) and is_map(bindings) do
205 | new_actions = Enum.map(actions, fn action -> replace_bindings(action, bindings) end)
206 | %{pnode | action: new_actions, bindings: bindings}
207 | end
208 |
209 | def replace_bindings(%Retex.Wme{} = action, bindings) when is_map(bindings) do
210 | populated =
211 | for {key, val} <- Map.from_struct(action), into: %{} do
212 | val = Map.get(bindings, val, val)
213 | {key, val}
214 | end
215 |
216 | struct(Retex.Wme, populated)
217 | end
218 |
219 | def replace_bindings(tuple, bindings) when is_map(bindings) and is_tuple(tuple) do
220 | List.to_tuple(
221 | for element <- Tuple.to_list(tuple) do
222 | if is_binary(element), do: Map.get(bindings, element, element), else: element
223 | end
224 | )
225 | end
226 |
227 | def replace_bindings(anything, _bindings) do
228 | anything
229 | end
230 |
231 | @spec add_token(Retex.t(), network_node(), Retex.Wme.t(), map, list(Retex.Token.t())) ::
232 | Retex.t()
233 | def add_token(
234 | %Retex{tokens: rete_tokens} = rete,
235 | current_node,
236 | _wme,
237 | _bindings,
238 | [_ | _] = tokens
239 | ) do
240 | node_tokens =
241 | rete_tokens
242 | |> Map.get(current_node.id, [])
243 | |> MapSet.new()
244 |
245 | all_tokens =
246 | node_tokens
247 | |> MapSet.new()
248 | |> MapSet.union(MapSet.new(tokens))
249 |
250 | new_tokens = Map.put(rete_tokens, current_node.id, all_tokens)
251 |
252 | %{rete | tokens: new_tokens}
253 | end
254 |
255 | def add_token(%Retex{tokens: rete_tokens} = rete, current_node, wme, bindings, tokens) do
256 | node_tokens =
257 | rete_tokens
258 | |> Map.get(current_node.id, [])
259 | |> MapSet.new()
260 |
261 | token = Token.new()
262 |
263 | token = %{
264 | token
265 | | wmem: wme,
266 | node: current_node.id,
267 | bindings: bindings
268 | }
269 |
270 | all_tokens =
271 | [token]
272 | |> MapSet.new()
273 | |> MapSet.union(node_tokens)
274 | |> MapSet.union(MapSet.new(tokens))
275 |
276 | new_tokens = Map.put(rete_tokens, current_node.id, all_tokens)
277 | %{rete | tokens: new_tokens}
278 | end
279 |
280 | @spec create_activation(Retex.t(), network_node(), Retex.Wme.t()) :: Retex.t()
281 | def create_activation(
282 | %__MODULE__{activations: activations, wme_activations: wme_activations} = rete,
283 | current_node,
284 | wme
285 | ) do
286 | node_activations = Map.get(activations, current_node.id, [])
287 | new_activations = [wme.id | node_activations]
288 | new_rete = %{rete | activations: Map.put(activations, current_node.id, new_activations)}
289 | previous_wme_activations = Map.get(wme_activations, wme.id, [])
290 |
291 | new_wme_activations =
292 | Map.put(wme_activations, wme.id, [current_node.id | previous_wme_activations])
293 |
294 | %{new_rete | wme_activations: new_wme_activations}
295 | end
296 |
297 | @spec propagate_activations(
298 | Retex.t(),
299 | network_node(),
300 | Retex.Wme.t(),
301 | map,
302 | list(Retex.Token.t())
303 | ) :: {Retex.t(), map}
304 | def propagate_activations(
305 | %Retex{graph: graph} = rete,
306 | %{} = current_node,
307 | %Retex.Wme{} = wme,
308 | bindings,
309 | new_tokens
310 | ) do
311 | graph
312 | |> Graph.out_neighbors(current_node)
313 | |> Enum.reduce({rete, bindings}, fn vertex, {network, bindings} ->
314 | propagate_activation(vertex, network, wme, bindings, new_tokens)
315 | end)
316 | end
317 |
318 | @spec propagate_activations(Retex.t(), network_node(), Retex.Wme.t(), map) :: {Retex.t(), map}
319 | def propagate_activations(
320 | %Retex{graph: graph} = rete,
321 | %{} = current_node,
322 | %Retex.Wme{} = wme,
323 | bindings
324 | ) do
325 | graph
326 | |> Graph.out_neighbors(current_node)
327 | |> Enum.reduce({rete, bindings}, fn vertex, {network, bindings} ->
328 | propagate_activation(vertex, network, wme, bindings)
329 | end)
330 | end
331 |
332 | defp propagate_activation(neighbor, rete, wme, bindings, tokens \\ []) do
333 | Protocol.Activation.activate(neighbor, rete, wme, bindings, tokens)
334 | end
335 |
336 | @spec deactivate_descendants(Retex.t(), network_node()) :: Retex.t()
337 | def deactivate_descendants(%Retex{activations: activations} = rete, %{} = current_node) do
338 | %{graph: graph} = rete
339 | children = Graph.out_neighbors(graph, current_node)
340 |
341 | Enum.reduce(children, rete, fn %type{} = vertex, network ->
342 | if type == Retex.Node.PNode do
343 | %{
344 | network
345 | | agenda: Enum.reject(network.agenda, fn pnode -> pnode.id == vertex.id end)
346 | }
347 | else
348 | new_network = %{network | activations: Map.put(activations, vertex.id, [])}
349 | deactivate_descendants(new_network, vertex)
350 | end
351 | end)
352 | end
353 |
354 | @spec continue_traversal(Retex.t(), map, network_node(), Retex.Wme.t(), list(Retex.Token.t())) ::
355 | {Retex.t(), map}
356 | def continue_traversal(
357 | %Retex{} = new_rete,
358 | %{} = new_bindings,
359 | %_{} = current_node,
360 | %Retex.Wme{} = wme,
361 | tokens
362 | ) do
363 | propagate_activations(new_rete, current_node, wme, new_bindings, tokens)
364 | end
365 |
366 | @spec continue_traversal(Retex.t(), map, network_node(), Retex.Wme.t()) :: {Retex.t(), map}
367 | def continue_traversal(
368 | %Retex{} = new_rete,
369 | %{} = new_bindings,
370 | %_{} = current_node,
371 | %Retex.Wme{} = wme
372 | ) do
373 | propagate_activations(new_rete, current_node, wme, new_bindings)
374 | end
375 |
376 | @spec stop_traversal(Retex.t(), map) :: {Retex.t(), map}
377 | def stop_traversal(%Retex{} = rete, %{} = bindings) do
378 | {rete, bindings}
379 | end
380 | end
381 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Retex
2 |
3 | 
4 |
5 | # The Rete Algorithm
6 |
7 | "The Rete Match Algorithm is an efficient method for comparing a large collection of patterns to a large collection of objects. It finds all the objects that match each pattern. The algorithm was developed for use in production system interpreters, and it has been used for systems containing from a few hundred to more than a thousand patterns and objects" - C. Forgy
8 |
9 | **Boilerplate/PoC of a version of the Rete Algorithm implementated in Elixir**
10 |
11 | Rete is a complex stateful algorithm, this is an attempt of reproducing it with some slight modifications, using a functional immutable language such as Elixir/Erlang. [Read more about Rete](http://www.csl.sri.com/users/mwfong/Technical/RETE%20Match%20Algorithm%20-%20Forgy%20OCR.pdf)
12 |
13 | ## Requirements
14 |
15 | - Erlang/OTP 24
16 | - Elixir 1.14.2 (compiled with Erlang/OTP 22)
17 |
18 | ## How does it work?
19 |
20 | The algorithm utilizes symbols to create an internal representation of the world. Each element in the real world is converted into a triple known as a "Working Memory Element" (`Retex.Wme.t()`), represented as {Entity, attribute, attribute_value}.
21 |
22 | The world is represented through facts (WMEs) and Rules. A Rule consists of two essential parts: the "given" (right side) and the "then" (left side).
23 |
24 | To perform inference, the rule generates a directed graph starting from a common and generic Root node, which branches out to form leaf nodes. The branches from the Root node correspond to the initial part of the WME, representing the working memory elements or "Entity". For instance, if we want to represent a customer's account status as "silver", we would encode it as "{Customer, account_status, silver}". Alternatively, with the use of a struct, we can achieve the same representation as Retex.Wme.new("Customer", "account status", "silver").
25 |
26 | Now, let's explore how this would appear when compiling the rete algorithm with Retex:
27 |
28 | ```mermaid
29 | flowchart
30 | 2332826675[==silver]
31 | 3108351631[Root]
32 | 3860425667[Customer]
33 | 3895425755[account_status]
34 | 3108351631 --> 3860425667
35 | 3860425667 --> 3895425755
36 | 3895425755 --> 2332826675
37 | ```
38 |
39 | **example nr. 1**
40 |
41 | Now, let's examine the graph, which consists of four nodes in the following order:
42 |
43 | 1. The Root node
44 | 1. This node serves as the root for all type nodes, such as Account, Customer, God, Table, and so on.
45 | 2. The Customer node
46 | 1. Also known as a Type node, it stores each known "type" of entity recognized by the algorithm.
47 | 3. The account_status node
48 | 1. Referred to as a Select node, it represents the attribute name of the entity being described.
49 | 4. the ==silver node
50 | 1. Known as a Test node, it includes the == symbol, indicating that the value of Customer.account_status is checked against "silver" as a literal string (tests can use all Elixir comparison symbols).
51 |
52 | By expanding this network, we can continue mapping various aspects of the real world using any desired triple. Let's consider the entity representing a Flight, specifically its number of miles. We can represent this as {Flight, miles, 100} to signify a flight with a mileage of 100. Now, let's incorporate this into our network and observe the resulting graph:
53 |
54 | Let's add this to our network and check what kind of graph we will get:
55 |
56 | ```mermaid
57 | flowchart
58 | 2102090852[==100]
59 | 2332826675[==silver]
60 | 3108351631[Root]
61 | 3801762854[miles]
62 | 3860425667[Customer]
63 | 3895425755[account_status]
64 | 4112061991[Flight]
65 | 3108351631 --> 3860425667
66 | 3108351631 --> 4112061991
67 | 3801762854 --> 2102090852
68 | 3860425667 --> 3895425755
69 | 3895425755 --> 2332826675
70 | 4112061991 --> 3801762854
71 | ```
72 |
73 | **example nr. 2**
74 |
75 | Now we begin to observe the modeling of more complex scenarios. Let's consider the addition of our first inference to the network, which involves introducing our first rule.
76 |
77 | The rule we want to encode states that when the Customer's account_status is "silver" and the Flight's miles are exactly "100," we should apply a discount to the Customer entity.
78 |
79 | Let's examine how our network will appear after incorporating this rule:
80 |
81 | ```mermaid
82 | flowchart
83 | 2102090852["==100"]
84 | 2332826675["==silver"]
85 | 2833714732["[{:Discount, :code, 50}]"]
86 | 3108351631["Root"]
87 | 3726656564["Join"]
88 | 3801762854["miles"]
89 | 3860425667["Customer"]
90 | 3895425755["account_status"]
91 | 4112061991["Flight"]
92 | 2102090852 --> 3726656564
93 | 2332826675 --> 3726656564
94 | 3108351631 --> 3860425667
95 | 3108351631 --> 4112061991
96 | 3726656564 --> 2833714732
97 | 3801762854 --> 2102090852
98 | 3860425667 --> 3895425755
99 | 3895425755 --> 2332826675
100 | 4112061991 --> 3801762854
101 | ```
102 |
103 | Now we have constructed our network, which possesses a symbolic representation of the world and describes the relationships between multiple entities and their values to trigger a rule. Notably, the last node in the graph is represented as {:Discount, :code, 50}.
104 |
105 | Let's examine how we can interpret this graph step by step:
106 |
107 | 1. At the first level, we encounter the Root node, which serves as a placeholder.
108 | 2. At the second level, we find the Flight and Customer nodes branching out from the Root node. It's important to note that they are at the same level.
109 | 3. Both the Flight and Customer nodes branch out only once since they each have only one attribute.
110 | 4. Each attribute node (==100 and ==silver) branches out once again to indicate that if we encounter the attribute Customer.account_status, we should verify that its value is indeed "silver."
111 | 5. The last two nodes (==100 and ==silver) both connect to a new anonymous node called the Join node.
112 | 6. The Join node branches out only once, leading to the right-hand side of the rule (also known as the production node).
113 |
114 | This structure of the graph allows us to represent and process complex relationships and conditions within our network.
115 |
116 | ## What are join nodes?
117 |
118 | Join nodes are also what is called "beta memory" in the original C. Forgy paper. To make it simple we can assert that they group together a set of conditions that need to be true in order for a rule to fire. In our last example, the rule is:
119 |
120 | ```
121 | # pseudocode
122 | given: Flight.miles == 100 and Customer.account_status == "silver"
123 | then: Discount.code == 50
124 | ```
125 |
126 | In the graph representation, the Join node corresponds to the "and" in the "given" part of the rule. Its purpose is to evaluate and combine the conditions associated with its two parent nodes. Notably, a Join node can only have and will always have exactly two parents (incoming edges), which is a crucial characteristic of its design.
127 |
128 | By utilizing Join nodes, the network is able to effectively represent complex conditions and evaluate them in order to trigger the corresponding rules.
129 |
130 | ## What are production nodes?
131 |
132 | Production nodes, as named in the Forgy paper, refer to the right-hand side of a rule (also known as the "given" part). These nodes are exclusively connected to one incoming Join node in the network.
133 |
134 | To clarify, the purpose of a production node is to represent the actions or outcomes specified by the rule. It captures the consequences that should occur when the conditions specified in the Join node's associated "given" part are met. This relationship ensures that the rule's right-hand side is only triggered when the conditions of the Join node are satisfied.
135 |
136 | ## How do we use all of that after we built the network?
137 |
138 | Once we have a graph like the following and we know how to read it let's imagine we want to use to make inference and so to understand if we can give out
139 | such discount code to our customer.
140 |
141 | ```mermaid
142 | flowchart
143 | 2102090852["==100"]
144 | 2332826675["==silver"]
145 | 2833714732["[{:Discount, :code, 50}]"]
146 | 3108351631["Root"]
147 | 3726656564["Join"]
148 | 3801762854["miles"]
149 | 3860425667["Customer"]
150 | 3895425755["account_status"]
151 | 4112061991["Flight"]
152 | 2102090852 --> 3726656564
153 | 2332826675 --> 3726656564
154 | 3108351631 --> 3860425667
155 | 3108351631 --> 4112061991
156 | 3726656564 --> 2833714732
157 | 3801762854 --> 2102090852
158 | 3860425667 --> 3895425755
159 | 3895425755 --> 2332826675
160 | 4112061991 --> 3801762854
161 | ```
162 |
163 | This process is called adding WMEs (working memory elements) to the network. As you might have already guessed there is very little difference between a WME and a part of a rule.
164 |
165 | `Retex` exposes the function `Retex.add_wme(t(), Retex.Wme.t())` which takes the network itself and a WME struct and tries to activate as many nodes as possible traversing the graph from the Root until each reachable branch executing a series of "tests" at each node. Let's see step by step how it would work.
166 |
167 | Let's rewrite that same graph adding some names to the edges so we can reference them in the description:
168 |
169 | ```mermaid
170 | flowchart
171 | 2102090852["==100"]
172 | 2332826675["==silver"]
173 | 2833714732["[{:Discount, :code, 50}]"]
174 | 3108351631["Root"]
175 | 3726656564["Join"]
176 | 3801762854["miles"]
177 | 3860425667["Customer"]
178 | 3895425755["account_status"]
179 | 4112061991["Flight"]
180 | 2102090852 --a--> 3726656564
181 | 2332826675 --b--> 3726656564
182 | 3108351631 --c--> 3860425667
183 | 3108351631 --d--> 4112061991
184 | 3726656564 --e--> 2833714732
185 | 3801762854 --f--> 2102090852
186 | 3860425667 --g--> 3895425755
187 | 3895425755 --h--> 2332826675
188 | 4112061991 --i--> 3801762854
189 | ```
190 |
191 | Let's see what happens when adding the following working memory element to the Retex algorithm `Retex.Wme.new(:Flight, :miles, 100`
192 |
193 | 1. Retex will receive the WME and start testing the network from the Root node which passes down anything as it doesn't test for anything
194 | 2. The `Root` branches out in `n` nodes (Flight and Customer)
195 | 1. the branch `d` will find a type node with value "Flight" and this is the first part of the WME so the test is passing
196 | 1. the next branch from `d` is `i` which connects Flight to `miles` and so we test that the second part of the triple is exactly `miles`: the test is passing again
197 | 1. the next branch from `i` is `f` which finds a test node `== 100` which is the case of our new WME and so the test is passing
198 | 1. next is `a` which connects to the `Join` node which needs to be tested: a test for a Join node asserts that all incoming connections are active (their test passed) and given that the branch `b` is not yet tested the traversal for now ends here and the Join remains only 50% activated
199 | 2. the second branch to test is `c` which connects to `Customer` and this is not matching `Flight` so we can't go any further
200 |
201 | After adding the WME `Retex.Wme.new(:Flight, :miles, 100)` the only active branches and nodes are d, i, f and a
202 |
203 | Our rule can't be activated because the parent node `Join` is not fully active yet and so it can't propagate the current WME to the production node (which is sad but fair)
204 |
205 | Let's see what happens when adding the following working memory element to the Retex algorithm `Retex.Wme.new(:Customer, :account_status, "silver")`
206 |
207 | 1. Retex will receive the WME and start testing the network from the Root node which passes down anything as it doesn't test for anything
208 | 2. The `Root` branches out in `n` nodes (Flight and Customer)
209 | 1. the branch `c` will find a type node with value "Customer" and this is the first part of the WME so the test is passing
210 | 1. the next branch from `c` is `g` which connects Customer to `account_status` and so we test that the second part of the triple is exactly `account_status`: the test is passing again
211 | 1. the next branch from `h` finds a test node `== "silver"` which is the case of our new WME and so the test is passing
212 | 1. next is `b` which connects to the `Join` node which needs to be tested: a test for a Join node asserts that all incoming connections are active (their test passed) and given that the branch `a` is also already active (we stored that in a map) we can continue the traversal
213 | 1. Now we find a production node which tells us that the Discount code can be applied
214 | 2. the second branch to test is `d` which doesn't match so we can stop the traversal
215 |
216 | After adding the WME `Retex.Wme.new(:Customer, :account_status, "silver")` all nodes are active and so the production node ends up in the agenda (just an elixir list to keep track of all production nodes which are activated)
217 |
218 | We have now done inference and found an applicable rule. All we need to do now is to add the new WME to the network to check if any other node can be activated in the same way.
219 |
220 | Now imagine adding more and more complex rules and following the same strategy to find activable production nodes. The conditions will all be joined by a `Join` (and) node
221 | and will point to a production.
222 |
223 | ## Concepts
224 |
225 | - Retex compiles the rules using a directed acyclic graph data structure
226 | - The activation of nodes is done using a [State Monad and Forward Chaining](https://www.researchgate.net/publication/303626297_Forward_Chaining_with_State_Monad).
227 | - A list of bindings is stored at each active node in order to generate complete matches from partial ones
228 |
229 | ## Installation
230 |
231 | ```elixir
232 | def deps do
233 | [
234 | {:retex, git: "https://github.com/lorenzosinisi/retex"}
235 | ]
236 | end
237 | ```
238 |
239 | ## Installation using the wrapper NeuralBridge
240 |
241 | If you want you can have a predefined generic DSL and the wrapper NeuralBridge so that you don't have to build the rest of the Expert System from zero
242 |
243 | ```elixir
244 | # or visit https://neuralbridge.fly.dev <- an online playground to create rules and add facts
245 | def deps do
246 | [
247 | {:neural_bridge, git: "https://github.com/lorenzosinisi/neural_bridge"}
248 | ]
249 | end
250 | ```
251 |
252 | ## Examples and usage with NeuralBridge (a wrapper around Retex)
253 |
254 | ### Generic inferred knowledge
255 |
256 | ```elixir
257 | alias NeuralBridge.{Engine, Rule}
258 | engine = Engine.new("test")
259 |
260 | rules = [
261 | Rule.new(
262 | id: 1,
263 | given: """
264 | Person's name is equal "bob"
265 | """,
266 | then: """
267 | Person's age is 23
268 | """
269 | ),
270 | Rule.new(
271 | id: 2,
272 | given: """
273 | Person's name is equal $name
274 | Person's age is equal 23
275 | """,
276 | then: fn production ->
277 | require Logger
278 | bindings = Map.get(production, :bindings)
279 | Logger.info(inspect(bindings))
280 | end
281 | )
282 | ]
283 |
284 | engine = Engine.add_rules(engine, rules)
285 | engine = Engine.add_facts(engine, "Person's name is \"bob\"")
286 | rule = List.first(engine.rule_engine.agenda)
287 | engine = Engine.apply_rule(engine, rule)
288 |
289 | Enum.each(engine.rule_engine.agenda, fn pnode ->
290 | Engine.apply_rule(engine, pnode)
291 | end)
292 | end # will log %{"$name" => "bob"}
293 |
294 | ```
295 |
296 | ### Medical diagnosis
297 |
298 | ```elixir
299 | alias NeuralBridge.{Engine, Rule}
300 | engine = Engine.new("doctor_AI")
301 |
302 | engine =
303 | Engine.add_rules(engine, [
304 | Rule.new(
305 | id: 1,
306 | given: """
307 | Patient's fever is greater 38.5
308 | Patient's name is equal $name
309 | Patient's generic_weakness is equal "Yes"
310 | """,
311 | then: """
312 | Patient's diagnosis is "flu"
313 | """
314 | ),
315 | Rule.new(
316 | id: 2,
317 | given: """
318 | Patient's fever is lesser 38.5
319 | Patient's name is equal $name
320 | Patient's generic_weakness is equal "No"
321 | """,
322 | then: """
323 | Patient's diagnosis is "all good"
324 | """
325 | )
326 | ])
327 |
328 | engine =
329 | Engine.add_facts(engine, """
330 | Patient's fever is 39
331 | Patient's name is "Aylon"
332 | Patient's generic_weakness is "Yes"
333 | """)
334 |
335 | ## contains Patient's diagnnosis
336 | [
337 | %_{
338 | action: [
339 | %Retex.Wme{
340 | identifier: "Patient",
341 | attribute: "diagnosis",
342 | value: "flu"
343 | }
344 | ],
345 | bindings: %{"$name" => "Aylon"}
346 | }
347 | ] = engine.rule_engine.agenda
348 |
349 | ```
350 |
351 | ## Test
352 |
353 | - Run `mix test`
354 |
355 | ## Benchmark adding 20k rules and triggering one
356 |
357 | - Run `elixir benchmark/rule_chain.exs`
358 |
359 | ## Warnings
360 |
361 | - Use at your own risk
362 | - This is just a template for complexer implementations of the described algorithms
363 |
364 | ## Resources and documentation
365 |
366 | - [Rete](https://cis.temple.edu/~giorgio/cis587/readings/rete.html)
367 | - [Retex at ElixirConf EU](https://www.youtube.com/watch?v=pvi5hURNzbk&ab_channel=CodeSync)
368 | - Retex is available as playground here: https://neuralbridge.fly.dev
369 |
370 | References:
371 |
372 | ```
373 | Rete: A Fast Algorithm for the Many Pattern/Many Object Pattern Match Problem* by
374 | Charles L. Forgy
375 | Department of Computer Science, Carnegie-Mellon University,
376 | Pittsburgh, PA 15213, U.S.A.
377 | ```
378 |
--------------------------------------------------------------------------------
/test/retex_test.exs:
--------------------------------------------------------------------------------
1 | defmodule RetexTest do
2 | use ExUnit.Case
3 | alias Retex.Facts
4 | import Facts
5 | doctest Retex
6 |
7 | defp create_rule(lhs: given, rhs: action) do
8 | %{
9 | given: given,
10 | then: action
11 | }
12 | end
13 |
14 | test "create a very simple rule of one condition" do
15 | given = [
16 | has_attribute(:Customer, :account_status, :==, "silver"),
17 | has_attribute(:Flight, :miles, :==, "100")
18 | ]
19 |
20 | action = [
21 | {:Discount, :code, 50}
22 | ]
23 |
24 | rule = create_rule(lhs: given, rhs: action)
25 |
26 | network = Retex.add_production(Retex.new(), rule)
27 |
28 | assert {:ok, _graph} = Retex.Serializer.serialize(network.graph)
29 | end
30 |
31 | test "adds the same rules twice" do
32 | given = [
33 | has_attribute(:Account, :status, :==, "silver"),
34 | has_attribute(:Account, :status, :!=, "outdated"),
35 | has_attribute(:Flight, :partner, :!=, true)
36 | ]
37 |
38 | action = [
39 | {:concession, 50}
40 | ]
41 |
42 | rule = create_rule(lhs: given, rhs: action)
43 |
44 | given_b = [
45 | has_attribute(:Account, :status, :==, "silver"),
46 | has_attribute(:Account, :status, :!=, "outdated"),
47 | has_attribute(:Family, :size, :>=, 12),
48 | has_attribute(:Flight, :partner, :!=, true)
49 | ]
50 |
51 | action_b = [
52 | {:concession, 110}
53 | ]
54 |
55 | rule_b = create_rule(lhs: given_b, rhs: action_b)
56 |
57 | network =
58 | Retex.new()
59 | |> Retex.add_production(rule)
60 | |> Retex.add_production(rule)
61 | |> Retex.add_production(rule_b)
62 | |> Retex.add_production(rule_b)
63 |
64 | assert 22 == Graph.edges(network.graph) |> Enum.count()
65 | assert 18 == Graph.vertices(network.graph) |> Enum.count()
66 | end
67 |
68 | test "add a production with existing attributes" do
69 | given = [
70 | has_attribute(:Account, :status, :==, "silver"),
71 | has_attribute(:Account, :status, :!=, "outdated"),
72 | has_attribute(:Flight, :partner, :!=, true)
73 | ]
74 |
75 | action = [
76 | {:concession, 50}
77 | ]
78 |
79 | rule = create_rule(lhs: given, rhs: action)
80 |
81 | given_b = [
82 | has_attribute(:Account, :status, :==, "silver"),
83 | has_attribute(:Account, :status, :!=, "outdated"),
84 | has_attribute(:Family, :size, :>=, 12),
85 | has_attribute(:Flight, :partner, :!=, true)
86 | ]
87 |
88 | action_b = [
89 | {:concession, 110}
90 | ]
91 |
92 | rule_b = create_rule(lhs: given_b, rhs: action_b)
93 |
94 | network =
95 | Retex.new()
96 | |> Retex.add_production(rule)
97 | |> Retex.add_production(rule_b)
98 |
99 | assert 22 == Graph.edges(network.graph) |> Enum.count()
100 | assert 18 == Graph.vertices(network.graph) |> Enum.count()
101 | end
102 |
103 | test "can convert the graph to a flowchart (mermaid)" do
104 | given = [
105 | has_attribute(:Account, :status, :==, "silver"),
106 | has_attribute(:Account, :status, :!=, "outdated"),
107 | has_attribute(:Flight, :partner, :!=, true)
108 | ]
109 |
110 | action = fn something -> something end
111 |
112 | rule = create_rule(lhs: given, rhs: action)
113 |
114 | given_b = [
115 | has_attribute(:Account, :status, :==, "silver"),
116 | has_attribute(:Account, :status, :!=, "outdated"),
117 | has_attribute(:Family, :size, :>=, 12),
118 | has_attribute(:Flight, :partner, :!=, true)
119 | ]
120 |
121 | action_b = fn something -> something end
122 |
123 | rule_b = create_rule(lhs: given_b, rhs: action_b)
124 |
125 | assert {:ok, _} =
126 | Retex.new()
127 | |> Retex.add_production(rule)
128 | |> Retex.add_production(rule_b)
129 | |> Map.get(:graph)
130 | |> Retex.Serializer.serialize()
131 | end
132 |
133 | test "add a production where the then clause is a function" do
134 | given = [
135 | has_attribute(:Account, :status, :==, "silver"),
136 | has_attribute(:Account, :status, :!=, "outdated"),
137 | has_attribute(:Flight, :partner, :!=, true)
138 | ]
139 |
140 | action = fn something -> something end
141 |
142 | rule = create_rule(lhs: given, rhs: action)
143 |
144 | given_b = [
145 | has_attribute(:Account, :status, :==, "silver"),
146 | has_attribute(:Account, :status, :!=, "outdated"),
147 | has_attribute(:Family, :size, :>=, 12),
148 | has_attribute(:Flight, :partner, :!=, true)
149 | ]
150 |
151 | action_b = fn something -> something end
152 |
153 | rule_b = create_rule(lhs: given_b, rhs: action_b)
154 |
155 | network =
156 | Retex.new()
157 | |> Retex.add_production(rule)
158 | |> Retex.add_production(rule_b)
159 |
160 | assert 22 == Graph.edges(network.graph) |> Enum.count()
161 | assert 18 == Graph.vertices(network.graph) |> Enum.count()
162 | end
163 |
164 | test "add a production" do
165 | given = [
166 | has_attribute(:Account, :status, :==, "silver"),
167 | has_attribute(:Account, :status, :!=, "outdated"),
168 | has_attribute(:Flight, :partner, :!=, true)
169 | ]
170 |
171 | action = [
172 | {:concession, 50}
173 | ]
174 |
175 | rule = create_rule(lhs: given, rhs: action)
176 |
177 | network =
178 | Retex.new()
179 | |> Retex.add_production(rule)
180 |
181 | assert 12 == Graph.edges(network.graph) |> Enum.count()
182 | assert 11 == Graph.vertices(network.graph) |> Enum.count()
183 | end
184 |
185 | describe "add_wme/2" do
186 | test "apply inference with rules in which we use isa statements that doesnt match" do
187 | wme = Retex.Wme.new(:Account, :status, :silver)
188 | wme_2 = Retex.Wme.new(:Account, :premium, true)
189 | wme_3 = Retex.Wme.new(:Family, :size, 10)
190 |
191 | given = [
192 | isa("$thing", :AccountB),
193 | has_attribute("$thing", :status, :==, "$a"),
194 | has_attribute("$thing", :premium, :==, true)
195 | ]
196 |
197 | action = [
198 | {"$thing", :account_status, "$a"}
199 | ]
200 |
201 | rule = create_rule(lhs: given, rhs: action)
202 |
203 | network =
204 | Retex.new()
205 | |> Retex.add_production(rule)
206 | |> Retex.add_wme(wme)
207 | |> Retex.add_wme(wme_2)
208 | |> Retex.add_wme(wme_3)
209 |
210 | assert network.agenda == []
211 | end
212 |
213 | test "an activation of a negative node will deactivate its descendants" do
214 | given = [
215 | isa("$thing", :Thing),
216 | is_not("$entity", :Flight)
217 | ]
218 |
219 | action = [
220 | Retex.Wme.new(:Account, :id, 1)
221 | ]
222 |
223 | rule = create_rule(lhs: given, rhs: action)
224 |
225 | wme = Retex.Wme.new(:Thing, :status, :silver)
226 | wme_1 = Retex.Wme.new(:Flight, :status, :silver)
227 |
228 | network =
229 | Retex.new()
230 | |> Retex.add_production(rule)
231 | |> Retex.add_wme(wme)
232 | |> Retex.add_wme(wme_1)
233 |
234 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action))
235 |
236 | assert [] == agenda
237 | end
238 |
239 | test "apply inference with a negated attribute, when the negative node is active" do
240 | given = [
241 | isa("$thing", :Thing),
242 | not_existing_attribute(:Thing, :status)
243 | ]
244 |
245 | action = [
246 | Retex.Wme.new(:Thing, :status, 1)
247 | ]
248 |
249 | rule = create_rule(lhs: given, rhs: action)
250 |
251 | wme = Retex.Wme.new(:Thing, :status, :gold)
252 |
253 | network =
254 | Retex.new()
255 | |> Retex.add_production(rule)
256 | |> Retex.add_wme(wme)
257 |
258 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action))
259 |
260 | assert [] == agenda
261 |
262 | wme_1 = Retex.Wme.new(:Thing, :id, 1)
263 | network = Retex.add_wme(network, wme_1)
264 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action))
265 |
266 | assert [] == agenda
267 | end
268 |
269 | test "apply inference with a negated attribute, when the negative node is not active" do
270 | given = [
271 | isa("$thing", :Thing),
272 | not_existing_attribute(:Thing, :status)
273 | ]
274 |
275 | action = [
276 | Retex.Wme.new(:Thing, :status, 1)
277 | ]
278 |
279 | rule = create_rule(lhs: given, rhs: action)
280 |
281 | wme = Retex.Wme.new(:Thing, :id, 1)
282 |
283 | network =
284 | Retex.new()
285 | |> Retex.add_production(rule)
286 | |> Retex.add_wme(wme)
287 |
288 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action))
289 |
290 | assert [[%Retex.Wme{attribute: :status, identifier: :Thing, value: 1}]] = agenda
291 |
292 | wme_1 = Retex.Wme.new(:Thing, :status, :silver)
293 | network = Retex.add_wme(network, wme_1)
294 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action))
295 |
296 | assert [] == agenda
297 | end
298 |
299 | test "apply inference with a negated condition, when the negative node is active" do
300 | given = [
301 | isa("$thing", :Thing),
302 | is_not("$entity", :Flight)
303 | ]
304 |
305 | action = [
306 | Retex.Wme.new(:Account, :id, 1)
307 | ]
308 |
309 | rule = create_rule(lhs: given, rhs: action)
310 |
311 | wme = Retex.Wme.new(:Flight, :status, :silver)
312 | wme_1 = Retex.Wme.new(:Thing, :status, :silver)
313 |
314 | network =
315 | Retex.new()
316 | |> Retex.add_production(rule)
317 | |> Retex.add_wme(wme)
318 | |> Retex.add_wme(wme_1)
319 |
320 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action))
321 |
322 | assert [] == agenda
323 | end
324 |
325 | test "apply inference with a negated condition" do
326 | given = [
327 | isa("$thing", :Thing),
328 | is_not("$entity", :Flight)
329 | ]
330 |
331 | action = [
332 | Retex.Wme.new(:Account, :id, 1)
333 | ]
334 |
335 | rule = create_rule(lhs: given, rhs: action)
336 |
337 | wme = Retex.Wme.new(:Thing, :status, :silver)
338 |
339 | network =
340 | Retex.new()
341 | |> Retex.add_production(rule)
342 | |> Retex.add_wme(wme)
343 |
344 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action))
345 |
346 | assert [
347 | [
348 | %Retex.Wme{
349 | attribute: :id,
350 | timestamp: nil,
351 | identifier: :Account,
352 | value: 1
353 | }
354 | ]
355 | ] = agenda
356 | end
357 |
358 | test "apply inference and replace variables in a WME" do
359 | given = [
360 | has_attribute(:Account, :status, :==, "$a"),
361 | has_attribute(:Account, :premium, :==, true)
362 | ]
363 |
364 | action = [
365 | Retex.Wme.new("$thing", :account_status, "$a")
366 | ]
367 |
368 | rule = create_rule(lhs: given, rhs: action)
369 |
370 | given_2 = [
371 | has_attribute(:AccountB, :status, :==, "$a_a"),
372 | has_attribute(:AccountB, :premium, :==, true)
373 | ]
374 |
375 | action_2 = [
376 | {"$thing_a", :account_status, "$a_a"}
377 | ]
378 |
379 | rule_2 = create_rule(lhs: given_2, rhs: action_2)
380 |
381 | given_3 = [
382 | has_attribute(:Account, :status, :==, "$a"),
383 | has_attribute(:Account, :premium, :==, true),
384 | has_attribute(:AccountB, :status, :==, "$a_a"),
385 | has_attribute(:AccountB, :premium, :==, true)
386 | ]
387 |
388 | action_3 = [
389 | {"$thing_3", :account_status, "$a_3"}
390 | ]
391 |
392 | rule_3 = create_rule(lhs: given_3, rhs: action_3)
393 |
394 | wme = Retex.Wme.new(:Account, :status, :silver)
395 | wme_2 = Retex.Wme.new(:Account, :premium, true)
396 | wme_3 = Retex.Wme.new(:Family, :size, 10)
397 |
398 | network =
399 | Retex.new()
400 | |> Retex.add_production(rule)
401 | |> Retex.add_production(rule_2)
402 | |> Retex.add_production(rule_3)
403 | |> Retex.add_wme(wme)
404 | |> Retex.add_wme(wme_2)
405 | |> Retex.add_wme(wme_3)
406 |
407 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action))
408 |
409 | assert [
410 | [
411 | %Retex.Wme{
412 | attribute: :account_status,
413 | timestamp: nil,
414 | identifier: "$thing",
415 | value: :silver
416 | }
417 | ]
418 | ] = agenda
419 |
420 | wme_4 = Retex.Wme.new(:AccountB, :premium, true)
421 | wme_5 = Retex.Wme.new(:AccountB, :status, true)
422 |
423 | network =
424 | network
425 | |> Retex.add_wme(wme_4)
426 | |> Retex.add_wme(wme_5)
427 |
428 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action)) |> Enum.count()
429 |
430 | assert 3 == agenda
431 | end
432 |
433 | test "return only the bindings of the fully activated rule" do
434 | given = [
435 | has_attribute(:Account, :status, :==, "$a"),
436 | has_attribute(:Account, :premium, :==, "$b"),
437 | has_attribute(:Account, :age, :==, "$a"),
438 | has_attribute(:Account, :age, :>, 21)
439 | ]
440 |
441 | action = [
442 | {:Account, :activated_1, "$a"}
443 | ]
444 |
445 | rule = create_rule(lhs: given, rhs: action)
446 |
447 | given_2 = [
448 | has_attribute(:Account, :status, :==, "$a"),
449 | filter("$a", :!==, :blue),
450 | has_attribute(:Account, :premium, :==, "$b"),
451 | has_attribute(:Account, :age, :>, 11)
452 | ]
453 |
454 | action_2 = [
455 | {:Account, :activated_2, "$a"}
456 | ]
457 |
458 | rule_2 = create_rule(lhs: given_2, rhs: action_2)
459 |
460 | wme = Retex.Wme.new(:Account, :status, :silver)
461 | wme_2 = Retex.Wme.new(:Account, :premium, true)
462 | wme_5 = Retex.Wme.new(:Account, :status, :blue)
463 | wme_3 = Retex.Wme.new(:Account, :age, 10)
464 | wme_4 = Retex.Wme.new(:Account, :status, :silver)
465 |
466 | network =
467 | Retex.new()
468 | |> Retex.add_production(rule)
469 | |> Retex.add_production(rule_2)
470 | |> Retex.add_wme(wme)
471 | |> Retex.add_wme(wme_2)
472 | |> Retex.add_wme(wme_3)
473 | |> Retex.add_wme(wme_4)
474 | |> Retex.add_wme(wme_5)
475 |
476 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action))
477 |
478 | assert agenda == []
479 |
480 | triggering = Retex.Wme.new(:Account, :age, 20)
481 |
482 | network = Retex.add_wme(network, triggering)
483 |
484 | agenda = network.agenda |> Enum.map(&Map.take(&1, [:bindings]))
485 |
486 | assert agenda == [%{bindings: %{"$a" => :silver, "$b" => true}}]
487 | end
488 |
489 | test "apply inference with rules in which we use isa statements" do
490 | given = [
491 | has_attribute(:Account, :status, :==, "$a"),
492 | has_attribute(:Account, :premium, :==, true)
493 | ]
494 |
495 | action = [
496 | {"$thing", :account_status, "$a"}
497 | ]
498 |
499 | rule = create_rule(lhs: given, rhs: action)
500 |
501 | given_2 = [
502 | has_attribute(:AccountB, :status, :==, "$a_a"),
503 | has_attribute(:AccountB, :premium, :==, true)
504 | ]
505 |
506 | action_2 = [
507 | {"$thing_a", :account_status, "$a_a"}
508 | ]
509 |
510 | rule_2 = create_rule(lhs: given_2, rhs: action_2)
511 |
512 | wme = Retex.Wme.new(:Account, :status, :silver)
513 | wme_2 = Retex.Wme.new(:Account, :premium, true)
514 | wme_3 = Retex.Wme.new(:Family, :size, 10)
515 |
516 | network =
517 | Retex.new()
518 | |> Retex.add_production(rule)
519 | |> Retex.add_production(rule_2)
520 | |> Retex.add_wme(wme)
521 | |> Retex.add_wme(wme_2)
522 | |> Retex.add_wme(wme_3)
523 |
524 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action))
525 |
526 | assert agenda == [[{"$thing", :account_status, :silver}]]
527 | end
528 |
529 | test "the bindings are returned upon node activation" do
530 | wme = Retex.Wme.new(:Account, :status, :silver)
531 | wme_2 = Retex.Wme.new(:Account, :premium, true)
532 | wme_3 = Retex.Wme.new(:Family, :size, 10)
533 |
534 | given = [
535 | has_attribute(:Account, :status, :==, "$a"),
536 | has_attribute(:Account, :premium, :==, true)
537 | ]
538 |
539 | action = [
540 | {"$thing", :account_status, "$a"}
541 | ]
542 |
543 | rule = create_rule(lhs: given, rhs: action)
544 |
545 | network =
546 | Retex.new()
547 | |> Retex.add_production(rule)
548 | |> Retex.add_wme(wme)
549 | |> Retex.add_wme(wme_2)
550 | |> Retex.add_wme(wme_3)
551 |
552 | agenda = network.agenda
553 | [pnode] = agenda
554 | assert pnode.bindings == %{"$a" => :silver}
555 | end
556 |
557 | test "apply inference with rules in which all elements are variables" do
558 | wme = Retex.Wme.new(:Account, :status, :silver)
559 | wme_2 = Retex.Wme.new(:Account, :premium, true)
560 | wme_3 = Retex.Wme.new(:Family, :size, 10)
561 |
562 | given = [
563 | has_attribute(:Account, :status, :==, "$a"),
564 | has_attribute(:Account, :premium, :==, true)
565 | ]
566 |
567 | action = [
568 | {"$thing", :account_status, "$a"}
569 | ]
570 |
571 | rule = create_rule(lhs: given, rhs: action)
572 |
573 | network =
574 | Retex.new()
575 | |> Retex.add_production(rule)
576 | |> Retex.add_wme(wme)
577 | |> Retex.add_wme(wme_2)
578 | |> Retex.add_wme(wme_3)
579 |
580 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action))
581 |
582 | [pnodes] = agenda
583 |
584 | assert pnodes == [{"$thing", :account_status, :silver}]
585 | end
586 |
587 | test "apply inference with the use of variables as types" do
588 | wme = Retex.Wme.new(:Account, :status, :silver)
589 | wme_2 = Retex.Wme.new(:Account, :premium, false)
590 | wme_3 = Retex.Wme.new(:Family, :size, 10)
591 |
592 | given = [
593 | has_attribute(:Account, :status, :==, "$a"),
594 | has_attribute(:Account, :premium, :==, false),
595 | has_attribute(:Family, :size, :==, "$c")
596 | ]
597 |
598 | action = [
599 | {"$thing", :account_status, "$a"}
600 | ]
601 |
602 | rule = create_rule(lhs: given, rhs: action)
603 |
604 | network =
605 | Retex.new()
606 | |> Retex.add_production(rule)
607 | |> Retex.add_wme(wme)
608 | |> Retex.add_wme(wme_2)
609 | |> Retex.add_wme(wme_3)
610 |
611 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action))
612 | assert agenda == [[{"$thing", :account_status, :silver}]]
613 | end
614 |
615 | test "apply inference with the use of variables and they DONT match" do
616 | wme = Retex.Wme.new(:Account, :status, :silver)
617 | wme_2 = Retex.Wme.new(:Family, :size, 10)
618 |
619 | given = [
620 | has_attribute(:Account, :status, :==, "$a"),
621 | has_attribute(:Family, :size, :==, "$a")
622 | ]
623 |
624 | action = [
625 | {:Flight, :account_status, "$a"}
626 | ]
627 |
628 | rule = create_rule(lhs: given, rhs: action)
629 |
630 | network =
631 | Retex.new()
632 | |> Retex.add_production(rule)
633 | |> Retex.add_wme(wme)
634 | |> Retex.add_wme(wme_2)
635 |
636 | agenda = network.agenda
637 |
638 | assert agenda == []
639 | end
640 |
641 | test "apply inference with the use of variables and they match" do
642 | wme = Retex.Wme.new(:Account, :status, :silver)
643 | wme_2 = Retex.Wme.new(:Family, :size, :silver)
644 |
645 | given = [
646 | has_attribute(:Account, :status, :==, "$a"),
647 | has_attribute(:Family, :size, :==, "$a")
648 | ]
649 |
650 | action = [
651 | {:Flight, :account_status, "$a"}
652 | ]
653 |
654 | rule = create_rule(lhs: given, rhs: action)
655 |
656 | network =
657 | Retex.new()
658 | |> Retex.add_production(rule)
659 | |> Retex.add_wme(wme)
660 | |> Retex.add_wme(wme_2)
661 |
662 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action))
663 |
664 | assert ^agenda = [[{:Flight, :account_status, :silver}]]
665 | end
666 |
667 | test "tokens are created" do
668 | wme = Retex.Wme.new(:Account, :status, :silver)
669 | wme_2 = Retex.Wme.new(:Family, :size, :silver)
670 |
671 | given = [
672 | has_attribute(:Account, :status, :==, "$a"),
673 | has_attribute(:Family, :size, :==, "$a")
674 | ]
675 |
676 | action = [
677 | {:Flight, :account_status, "$a"}
678 | ]
679 |
680 | rule = create_rule(lhs: given, rhs: action)
681 |
682 | network =
683 | Retex.new()
684 | |> Retex.add_production(rule)
685 | |> Retex.add_wme(wme)
686 | |> Retex.add_wme(wme_2)
687 |
688 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action))
689 |
690 | assert agenda == [
691 | [{:Flight, :account_status, :silver}]
692 | ]
693 |
694 | assert network.tokens !== nil
695 | end
696 |
697 | test "apply inference with the use of variables with two rules sharing var names and attribute names" do
698 | wme = Retex.Wme.new(:Account, :status, :silver)
699 | wme_2 = Retex.Wme.new(:Family, :size, 10)
700 | wme_4 = Retex.Wme.new(:Account, :status, 10)
701 |
702 | given = [
703 | has_attribute(:Account, :status, :==, "$a"),
704 | has_attribute(:Family, :size, :==, 10)
705 | ]
706 |
707 | action = [
708 | {:Flight, :account_status, "$a"}
709 | ]
710 |
711 | rule = create_rule(lhs: given, rhs: action)
712 |
713 | given_2 = [
714 | has_attribute(:Account, :status, :==, "$a"),
715 | has_attribute(:Family, :size, :==, "$a")
716 | ]
717 |
718 | action_2 = [
719 | {:Flight, :account_status_a, "$a"}
720 | ]
721 |
722 | rule_2 = create_rule(lhs: given_2, rhs: action_2)
723 |
724 | network =
725 | Retex.new()
726 | |> Retex.add_production(rule)
727 | |> Retex.add_production(rule_2)
728 | |> Retex.add_wme(wme)
729 | |> Retex.add_wme(wme_2)
730 |
731 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action))
732 |
733 | assert agenda == [[{:Flight, :account_status, :silver}]]
734 |
735 | network =
736 | network
737 | |> Retex.add_wme(wme_4)
738 |
739 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action)) |> Enum.sort()
740 |
741 | assert agenda == [
742 | [{:Flight, :account_status, :silver}],
743 | [{:Flight, :account_status_a, 10}]
744 | ]
745 | end
746 |
747 | test "apply inference with the use of variables with two rules sharing var names" do
748 | wme = Retex.Wme.new(:Account, :status, :silver)
749 | wme_2 = Retex.Wme.new(:Family, :size, 10)
750 | wme_3 = Retex.Wme.new(:Family, :size_a, 10)
751 | wme_4 = Retex.Wme.new(:Account, :status_a, 10)
752 |
753 | given = [
754 | has_attribute(:Account, :status, :==, "$a"),
755 | has_attribute(:Family, :size, :==, 10),
756 | has_attribute(:Account, :status, :!=, 10)
757 | ]
758 |
759 | action = [
760 | {:Flight, :account_status, "$a"}
761 | ]
762 |
763 | rule = create_rule(lhs: given, rhs: action)
764 |
765 | given_2 = [
766 | has_attribute(:Account, :status_a, :==, "$a"),
767 | has_attribute(:Family, :size_a, :==, "$a")
768 | ]
769 |
770 | action_2 = [
771 | {:Flight, :account_status_a, "$a"}
772 | ]
773 |
774 | rule_2 = create_rule(lhs: given_2, rhs: action_2)
775 |
776 | network =
777 | Retex.new()
778 | |> Retex.add_production(rule)
779 | |> Retex.add_production(rule_2)
780 | |> Retex.add_wme(wme)
781 | |> Retex.add_wme(wme_2)
782 |
783 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action))
784 |
785 | assert agenda == [[{:Flight, :account_status, :silver}]]
786 |
787 | network =
788 | network
789 | |> Retex.add_wme(wme_3)
790 | |> Retex.add_wme(wme_4)
791 |
792 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action))
793 |
794 | assert agenda == [
795 | [{:Flight, :account_status_a, 10}],
796 | [{:Flight, :account_status, :silver}]
797 | ]
798 | end
799 |
800 | test "apply inference with the use of variables" do
801 | wme = Retex.Wme.new(:Account, :status, :silver)
802 | wme_2 = Retex.Wme.new(:Family, :size, 10)
803 |
804 | given = [
805 | has_attribute(:Account, :status, :==, "$a"),
806 | has_attribute(:Family, :size, :==, 10)
807 | ]
808 |
809 | action = [
810 | {:Flight, :account_status, "$a"}
811 | ]
812 |
813 | rule = create_rule(lhs: given, rhs: action)
814 |
815 | network =
816 | Retex.new()
817 | |> Retex.add_production(rule)
818 | |> Retex.add_wme(wme)
819 | |> Retex.add_wme(wme_2)
820 |
821 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action))
822 | assert agenda == [[{:Flight, :account_status, :silver}]]
823 | end
824 |
825 | test "add a new wme, trigger production" do
826 | wme = Retex.Wme.new(:Account, :status, :silver)
827 |
828 | given = [
829 | has_attribute(:Account, :status, :==, :silver)
830 | ]
831 |
832 | action = [
833 | {:Flight, :account_status, "silver"}
834 | ]
835 |
836 | rule = create_rule(lhs: given, rhs: action)
837 |
838 | network =
839 | Retex.new()
840 | |> Retex.add_production(rule)
841 | |> Retex.add_wme(wme)
842 |
843 | agenda = network.agenda |> Enum.map(&Map.get(&1, :action))
844 |
845 | assert agenda == [[{:Flight, :account_status, "silver"}]]
846 | end
847 |
848 | test "add a new wme" do
849 | wme = Retex.Wme.new(:Account, :status, "silver")
850 |
851 | network =
852 | Retex.new()
853 | |> Retex.add_wme(wme)
854 |
855 | assert network.wmes |> Enum.count() == 1
856 | end
857 | end
858 | end
859 |
--------------------------------------------------------------------------------