ExDatalog.Program (ExDatalog v0.2.0)

Copy Markdown View Source

Builder for constructing Datalog programs.

A program holds:

  • Relations — named schemas with arity and type information.
  • Facts — ground tuples asserted as true for a given relation.
  • Rules — inference rules that derive new facts from existing ones.

Relation names are string keys stored in a map. They do not collide with internal atoms like :positive, :negative, :wildcard, or :constraint because those atoms appear in tuple positions ({:positive, atom}), not as map keys.

Programs are built using a pipeline of builder functions. Structural validation (arity checking, relation existence) is done at build time by add_relation/3, add_fact/3, and add_rule/2. On failure these return {:error, String.t()} with a human-readable message.

Error propagation in pipelines

Because builder functions return t() | {:error, String.t()}, a failed step will short-circuit the rest of the pipeline: the {:error, _} tuple passes through unchanged. This means you can pipe freely and check for errors at the end:

{:ok, result} =
  Program.new()
  |> Program.add_relation("edge", [:atom, :atom])
  |> Program.add_relation("path", [:atom, :atom])
  |> Program.add_fact("edge", [:a, :b])
  |> Program.add_rule(...)
  |> ExDatalog.query()

If add_relation/3 fails, the {:error, msg} tuple flows through add_fact/3 and add_rule/2 without raising, and ExDatalog.query/1 will detect the error struct and return {:error, [msg]}.

Semantic validation (variable safety, stratification, constraint binding) is done separately by ExDatalog.Validator.validate/1, which returns {:error, [ExDatalog.Validator.Error.t()]} with structured error structs.

Note: builder methods perform a subset of the same checks as the validator (relation existence, arity). This is intentional: the builder provides early feedback for interactive construction, while the validator is the canonical source of truth and catches issues the builder cannot (e.g., programs assembled by directly modifying the struct, which bypasses builder validation).

Example

iex> alias ExDatalog.{Program, Atom, Rule, Term}
iex> program =
...>   Program.new()
...>   |> Program.add_relation("parent", [:atom, :atom])
...>   |> Program.add_relation("ancestor", [:atom, :atom])
...>   |> Program.add_fact("parent", [:alice, :bob])
...>   |> Program.add_fact("parent", [:bob, :carol])
...>   |> Program.add_rule(
...>        Rule.new(
...>          Atom.new("ancestor", [Term.var("X"), Term.var("Y")]),
...>          [{:positive, Atom.new("parent", [Term.var("X"), Term.var("Y")])}]
...>        )
...>      )
iex> length(program.facts) == 2
true
iex> length(program.rules) == 1
true

Summary

Functions

Adds a ground fact to the program.

Adds a relation schema to the program.

Adds a rule to the program.

Returns true if the relation is defined in the program.

Creates a new, empty Datalog program.

Returns the schema for a relation, or nil if not defined.

Types

fact_values()

@type fact_values() :: [ExDatalog.Term.value()]

ir_type()

@type ir_type() :: :integer | :string | :atom | :any

relation_name()

@type relation_name() :: String.t()

relation_schema()

@type relation_schema() :: %{arity: non_neg_integer(), types: [ir_type()]}

t()

@type t() :: %ExDatalog.Program{
  facts: [{relation_name(), fact_values()}],
  relations: %{required(relation_name()) => relation_schema()},
  rules: [ExDatalog.Rule.t()]
}

Functions

add_fact(program, relation, values)

@spec add_fact(t(), relation_name(), fact_values()) :: t() | {:error, String.t()}

Adds a ground fact to the program.

The relation must be declared via add_relation/3 and the number of values must match the relation's arity.

Returns {:error, reason} if:

  • The relation is not defined.
  • The arity of values does not match the relation schema.
  • A value in values is not an integer, string, or atom (floats are not supported).

Examples

iex> alias ExDatalog.Program
iex> program = Program.new() |> Program.add_relation("parent", [:atom, :atom])
iex> Program.add_fact(program, "parent", [:alice, :bob])
%ExDatalog.Program{
  relations: %{"parent" => %{arity: 2, types: [:atom, :atom]}},
  facts: [{"parent", [:alice, :bob]}],
  rules: []
}

iex> alias ExDatalog.Program
iex> program = Program.new() |> Program.add_relation("parent", [:atom, :atom])
iex> {:error, _} = Program.add_fact(program, "unknown", [:alice])
{:error, "relation \"unknown\" is not defined"}

add_relation(program, name, types)

@spec add_relation(t(), relation_name(), [ir_type()]) :: t() | {:error, String.t()}

Adds a relation schema to the program.

types is a list of type atoms (:integer, :string, :atom, :any) with length equal to the arity of the relation.

Returns {:error, reason} if:

  • name is empty.
  • types is empty.
  • The relation already exists.

Examples

iex> ExDatalog.Program.add_relation(ExDatalog.Program.new(), "parent", [:atom, :atom])
%ExDatalog.Program{
  relations: %{"parent" => %{arity: 2, types: [:atom, :atom]}},
  facts: [],
  rules: []
}

iex> {:error, _} = ExDatalog.Program.add_relation(ExDatalog.Program.new(), "", [:atom])
{:error, "relation name must be a non-empty string"}

add_rule(program, rule)

@spec add_rule(t(), ExDatalog.Rule.t()) :: t() | {:error, String.t()}

Adds a rule to the program.

Performs structural validation:

  • The head relation must be declared.
  • The head arity must match the relation schema.
  • All body atoms must reference declared relations with matching arities.

Semantic validation (variable safety, stratification) is deferred to ExDatalog.Validator.

Returns {:error, reason} if any structural check fails.

Examples

iex> alias ExDatalog.{Program, Rule, Atom, Term}
iex> program =
...>   Program.new()
...>   |> Program.add_relation("parent", [:atom, :atom])
...>   |> Program.add_relation("ancestor", [:atom, :atom])
iex> rule = Rule.new(
...>   Atom.new("ancestor", [Term.var("X"), Term.var("Y")]),
...>   [{:positive, Atom.new("parent", [Term.var("X"), Term.var("Y")])}]
...> )
iex> result = Program.add_rule(program, rule)
iex> length(result.rules) == 1
true

has_relation?(program, name)

@spec has_relation?(t(), relation_name()) :: boolean()

Returns true if the relation is defined in the program.

Examples

iex> alias ExDatalog.Program
iex> program = Program.new() |> Program.add_relation("parent", [:atom, :atom])
iex> Program.has_relation?(program, "parent")
true

iex> alias ExDatalog.Program
iex> Program.has_relation?(Program.new(), "unknown")
false

new()

@spec new() :: t()

Creates a new, empty Datalog program.

Examples

iex> ExDatalog.Program.new()
%ExDatalog.Program{relations: %{}, facts: [], rules: []}

relation(program, name)

@spec relation(t(), relation_name()) :: relation_schema() | nil

Returns the schema for a relation, or nil if not defined.

Examples

iex> alias ExDatalog.Program
iex> program = Program.new() |> Program.add_relation("parent", [:atom, :atom])
iex> Program.relation(program, "parent")
%{arity: 2, types: [:atom, :atom]}

iex> alias ExDatalog.Program
iex> Program.relation(Program.new(), "unknown")
nil