Feel.Rules (feel v0.1.0)

Copy Markdown View Source

Questions about a table of rules whose entries are unary tests, the input entries of a decision table: does the table cover the inputs, do two rules share an input, is a rule never reached.

These queries need no DMN model: each column has a type for its input and each rule has a unary test for each column ("[18..150]", "\"gold\"", "-"). A rule is then a set of inputs, and the questions are about sets, which Feel.Type.Verdict answers (D-158):

  • coverage/3 is whether every input of the columns matches a rule;
  • overlaps/3 is the pairs of rules that match a common input (the hit policy unique needs none);
  • dead/3 is, for each rule, whether the rules before it take every input it could take (the policy first).

Each answer is proved or refuted with an input, or it is :unknown. A rule is bounded from outside and from inside by two types, as the analysis of unary tests gives them: the inputs it may match (Feel.Analysis.Refinement.restrict/2, a superset) and the inputs it certainly matches (Feel.Analysis.Refinement.certain/2, a subset). A table covers the inputs if they are within what the rules certainly match; an input that is in no rule's superset shows it does not; two rules are apart if their supersets are, and share an input that is in both subsets.

The entries that are read exactly are the comparisons of the input with one number, string, boolean, date or duration, an interval, a list of them, - and not(...) of those. An entry that is not (a name, a function, a time) is not a subset for certain, so a table with one may be :unknown where a person can see the answer; and a name makes its rule open: it may match any input, and certainly none. Whether a rule matches null is found by running its entry on null.

The columns are {name, type} with the type a union of the input (Feel.Type.Union, for example Feel.Type.Union.from_spec(:number) or one narrowed by its allowed values); a rule is a list of sources, one for each column in order. An entry may instead be :unknown when its meaning depends on bindings or extensions that the caller cannot preserve in source preparation. It may match any input of its column, including null, and certainly none; other entries and rules can still establish a conclusion. An input is a map from the name of a column to a value.

All three queries accept an optional :types map of named types, with the same descriptors as Feel.Scope.new/2. Supply the original type definitions when a source uses instance of, including aliases and qualified or multiword names: both parsing and type resolution depend on this map. These types remain available even when analysis has folded away the original type reference. Source entries are prepared without the caller's value bindings and without its extensions. The caller must supply the effective runtime test and use :unknown when that meaning cannot be preserved. This module does not resolve DMN inputValues or recognize model-local function aliases. Dmn.Compiler.Checks performs that adaptation from prepared table IR (DMN DD-43). An unknown answer produces no proof; absence of a reported pair alone is meaningful only when overlaps/2 actually returns {:ok, []}, rather than an error or pairs whose verdict is unknown.

iex> number = Feel.Type.Union.from_spec(:number) |> List.delete(:null)
iex> columns = [{"age", number}]
iex> Feel.Rules.coverage(columns, [["< 18"], [">= 18"]])
:covered
iex> {:uncovered, %{"age" => age}} = Feel.Rules.coverage(columns, [["< 18"], ["> 18"]])
iex> Decimal.equal?(age, 18)
true
iex> Feel.Rules.overlaps(columns, [["< 18"], [">= 18"]])
{:ok, []}
iex> {:ok, [%{rules: {0, 1}, verdict: {:overlap, _input}}]} = Feel.Rules.overlaps(columns, [["< 10"], ["[5..30]"], [">= 40"]])
iex> {:ok, [{0, {:live, _input}}, {1, :dead}]} = Feel.Rules.dead(columns, [["> 5"], ["> 10"]])

Summary

Types

An input of the table: the name of a column and the type of its values.

An input: a value for each column.

Named types used to parse and resolve source entries, including aliases and qualified names.

What was wrong with a rule: its position, the column, and the error of the parse.

A rule: a unary-test source or :unknown for each column, in the order of the columns.

Functions

Whether every input of the columns matches a rule.

For each rule, in order: :dead if the rules before it take every input it could take, {:live, input} with an input it takes that no rule before it can, or :unknown. Pass named type definitions with types: map.

The pairs of rules that are not shown to be apart, each with {:overlap, input} if an input matches both, or :unknown if none was found. A table where every pair is apart is {:ok, []}. Pass named type definitions with types: map.

Types

column()

@type column() :: {String.t(), Feel.Type.Union.t()}

An input of the table: the name of a column and the type of its values.

input()

@type input() :: %{optional(String.t()) => term()}

An input: a value for each column.

option()

@type option() :: {:types, %{optional(String.t()) => Feel.Type.t()}}

Named types used to parse and resolve source entries, including aliases and qualified names.

problem()

@type problem() ::
  {:error, {:entry, non_neg_integer(), String.t(), Feel.Error.t()}}
  | {:error, :columns}

What was wrong with a rule: its position, the column, and the error of the parse.

rule()

@type rule() :: [String.t() | :unknown]

A rule: a unary-test source or :unknown for each column, in the order of the columns.

Functions

coverage(columns, rules, options \\ [])

@spec coverage([column()], [rule()], [option()]) ::
  :covered | {:uncovered, input()} | :unknown | problem()

Whether every input of the columns matches a rule.

:covered is a proof. {:uncovered, input} is an input that no rule can match. :unknown is neither (the table is too large for the search, or an entry is not read exactly). Pass named type definitions with types: map.

dead(columns, rules, options \\ [])

@spec dead([column()], [rule()], [option()]) ::
  {:ok, [{non_neg_integer(), :dead | {:live, input()} | :unknown}]} | problem()

For each rule, in order: :dead if the rules before it take every input it could take, {:live, input} with an input it takes that no rule before it can, or :unknown. Pass named type definitions with types: map.

overlaps(columns, rules, options \\ [])

@spec overlaps([column()], [rule()], [option()]) ::
  {:ok,
   [
     %{
       rules: {non_neg_integer(), non_neg_integer()},
       verdict: {:overlap, input()} | :unknown
     }
   ]}
  | problem()

The pairs of rules that are not shown to be apart, each with {:overlap, input} if an input matches both, or :unknown if none was found. A table where every pair is apart is {:ok, []}. Pass named type definitions with types: map.