Migrating from the Builder API to the DSL

Copy Markdown View Source

ExDatalog v0.4.0 introduces an Ecto-inspired Schema DSL as the recommended way to define Datalog programs. The builder API (Program.add_relation, Program.add_fact, Program.add_rule) remains fully supported as the lower-level interface.

This guide shows how to migrate existing builder-API code to the DSL.

Relation Declarations

Builder API

program =
  Program.new()
  |> Program.add_relation("parent", [:atom, :atom])
  |> Program.add_relation("ancestor", [:atom, :atom])

DSL

defmodule FamilyRules do
  use ExDatalog.Schema

  relation :parent do
    field :parent, :atom
    field :child, :atom
  end

  relation :ancestor do
    field :ancestor, :atom
    field :descendant, :atom
  end
end

Facts

Builder API

program =
  program
  |> Program.add_fact("parent", [:alice, :bob])
  |> Program.add_fact("parent", [:bob, :carol])

DSL

fact parent(:alice, :bob)
fact parent(:bob, :carol)

# Or bulk:
facts :parent do
  row :alice, :bob
  row :bob, :carol
end

Rules

Builder API (struct-based)

Program.add_rule(program,
  Rule.new(
    Atom.new("ancestor", [Term.var("X"), Term.var("Y")]),
    [{:positive, Atom.new("parent", [Term.var("X"), Term.var("Y")])}]
  )
)

Builder API (shorthand)

Program.add_rule(program,
  {"ancestor", [:X, :Y]},
  [{:positive, {"parent", [:X, :Y]}}]
)

DSL

rule ancestor(X, Y) do
  parent(X, Y)
end

Lowercase variables are logic variables. Atoms starting with : are constants. _ is a wildcard.

Negation

Builder API

Program.add_rule(program, {"bachelor", [:X]}, [
  {:positive, {"male", [:X]}},
  {:negative, {"married", [:X, :_]}}
])

DSL

rule bachelor(P) do
  male(P)
  not_ married(P, _)
end

Constraints

Builder API

Program.add_rule(program,
  {"high_earner", [:X]},
  [{:positive, {"income", [:X, :S]}}],
  [{:gt, :S, 100_000}]
)

DSL

rule high_earner(P) do
  income(P, S)
  gt(S, 100_000)
end

Supported constraint names in the DSL: eq, neq, gt, gte, lt, lte, add, sub, mul, div, is_integer, is_binary, is_atom, starts_with, contains, member.

Materialization

Builder API

{:ok, knowledge} = ExDatalog.materialize(program)

DSL

{:ok, knowledge} = FamilyRules.materialize()

Both accept options: FamilyRules.materialize(iteration_limit: 100)

Queries

The DSL adds a query capability that doesn't have a builder-API equivalent:

query :descendants_of_alice do
  find Y
  where ancestor(:alice, Y)
end

{:ok, knowledge} = FamilyRules.materialize()
FamilyRules.query(:descendants_of_alice, knowledge)
#=> [:bob, :carol]

The equivalent builder-API code would use Knowledge.match/3 directly:

knowledge
|> Knowledge.match("ancestor", [:alice, :_])
|> MapSet.to_list()
|> Enum.map(fn {_, y} -> y end)

Mixing Both APIs

The DSL's program/0 returns a standard ExDatalog.Program struct. You can extend it with the builder API:

program = FamilyRules.program()

extended =
  program
  |> Program.add_fact("parent", [:carol, :dave])
  |> Program.add_rule(
    Rule.new(
      Atom.new("ancestor", [Term.var("X"), Term.var("Z")]),
      [{:positive, Atom.new("parent", [Term.var("X"), Term.var("Y")])},
       {:positive, Atom.new("ancestor", [Term.var("Y"), Term.var("Z")])}]
    )
  )

{:ok, knowledge} = ExDatalog.materialize(extended)

Variable Conventions

PatternBuilder APIDSL
Logic variableTerm.var("X") or :XX (lowercase in DSL)
ConstantTerm.from(:alice) or :alice:alice
WildcardTerm.from(:_) or :__ or wildcard()