This tutorial introduces the Aletheia language itself — the Prolog
dialect — independent of how it's embedded in a host application.
Every example here can be pasted straight into iex -S mix from a
checkout of this repository (assuming a database db, built the way
each section shows), or into the REPL (mix run -e 'Aletheia.Repl.start([])'). If you want to learn embedding Aletheia
into an Elixir application instead — adding the dependency, building a
database, running the REPL — see the
library tutorial for that side. For a systematic,
lookup-style reference once you know your way around, see
ALETHEIA.md.
1. Your first program
Aletheia programs are made of facts and rules, exactly like any
other Prolog. Load some source with consult_string/2 (or consult/1
for a file), then ask questions of it with query/2:
{:ok, db} = Aletheia.consult_string("""
parent(tom, bob).
parent(tom, liz).
parent(bob, ann).
parent(bob, pat).
""")
Aletheia.query("parent(tom, X)", db)
#=> {:ok, [%{"X" => :bob}, %{"X" => :liz}]}query/2 returns every solution, as a list of %{"VarName" => value} maps — one map per way the goal can succeed, in the order
Aletheia found them (declaration order, depth-first). Variables you
don't care about can stay anonymous with _:
Aletheia.query("parent(tom, _)", db)
#=> {:ok, [%{}, %{}]} -- two solutions, no variables bound in eitherIf you only want the first solution, use query_once/2:
Aletheia.query_once("parent(tom, X)", db)
#=> {:ok, %{"X" => :bob}}2. Rules and recursion
A rule's head and body share whatever variables they mention; the
comma is real Prolog conjunction (a genuine xfy(1000) operator here,
not just grammar-level punctuation — see the
reference if you're curious why that matters):
{:ok, db} = Aletheia.consult_string("""
parent(tom, bob).
parent(tom, liz).
parent(bob, ann).
parent(bob, pat).
parent(pat, jim).
grandparent(X, Z) :- parent(X, Y), parent(Y, Z).
ancestor(X, Y) :- parent(X, Y).
ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y).
""")
Aletheia.query("grandparent(tom, Who)", db)
#=> {:ok, [%{"Who" => :ann}, %{"Who" => :pat}]}
Aletheia.query("ancestor(tom, Who)", db)
#=> {:ok, [%{"Who" => :bob}, %{"Who" => :liz}, %{"Who" => :ann},
# %{"Who" => :pat}, %{"Who" => :jim}]}ancestor/2 recurses through parent/2 exactly the way you'd expect
from any Prolog system — Aletheia's engine is a real SLD-resolution
loop over a lazy search tree, not a special-cased evaluator.
3. Arithmetic
is/2 evaluates the right-hand side as an arithmetic expression and
unifies the result with the left-hand side, with the standard operator
precedence (*// bind tighter than +/-, unary minus tighter
still):
Aletheia.query("X is 2 + 3 * 4", Episteme.Database.new())
#=> {:ok, [%{"X" => 14}]}
Aletheia.query("X is 7 // 2", Episteme.Database.new())
#=> {:ok, [%{"X" => 3}]}The six arithmetic comparisons (=:=, =\=, <, >, =<, >=)
evaluate both sides the same way is/2 does:
{:ok, db} = Aletheia.consult_string("""
classify(X, positive) :- X > 0.
classify(0, zero).
classify(X, negative) :- X < 0.
""")
Aletheia.query("classify(5, What)", db)
#=> {:ok, [%{"What" => :positive}]}4. Control: disjunction and if-then-else
; is disjunction, -> is if-then; together, (Cond -> Then ; Else)
is if-then-else, committing to the first solution of Cond:
{:ok, db} = Aletheia.consult_string("""
sign(X, R) :- (X > 0 -> R = positive ; X < 0 -> R = negative ; R = zero).
""")
Aletheia.query("sign(5, R)", db)
#=> {:ok, [%{"R" => :positive}]}
Aletheia.query("sign(0, R)", db)
#=> {:ok, [%{"R" => :zero}]}5. Cut
! commits to every choice made since entering the current clause —
no more alternatives for that clause's own remaining goals, and no
more sibling clauses for that predicate. This is stricter than "stop
after the first solution" (that's once/1); the difference shows up
the moment a cut is followed by something that fails:
{:ok, db} = Aletheia.consult_string("""
p(1) :- !, fail.
p(2).
""")
Aletheia.query("p(X)", db)
#=> {:ok, []}p(X) fails outright: cut commits to clause 1 before fail ever
runs, so clause 2 is never tried — a once/1-based cut would instead
still fall through to p(2). See
ALETHEIA.md#cut for the full set of cut-scoping
rules (and why cut inside a called predicate never affects its
caller).
6. Negation as failure
\+ Goal succeeds exactly when Goal has no solution, binding
nothing either way:
{:ok, db} = Aletheia.consult_string("""
even(X) :- 0 is X mod 2.
odd(X) :- \\+ even(X).
""")
Aletheia.query("odd(3)", db)
#=> {:ok, [%{}]}
Aletheia.query("odd(4)", db)
#=> {:ok, []}7. Exceptions
throw/1 raises a term; catch(Goal, Catcher, Recovery) runs Goal,
and if it throws something that unifies with Catcher, runs Recovery
instead of propagating the exception further:
{:ok, db} = Aletheia.consult_string("""
safe_div(X, Y, R) :- catch(R is X / Y, error(_, _), R = undefined).
""")
Aletheia.query("safe_div(10, 2, R)", db)
#=> {:ok, [%{"R" => 5}]}
Aletheia.query("safe_div(10, 0, R)", db)
#=> {:ok, [%{"R" => :undefined}]}An exception nothing in your program catches surfaces as {:error, term} from Aletheia.query/2 itself, rather than crashing your
Elixir process.
8. Lists
Lists use native [H|T] syntax throughout, backed by real Elixir lists:
Aletheia.query("append([1, 2], [3, 4], X)", Episteme.Database.new())
#=> {:ok, [%{"X" => [1, 2, 3, 4]}]}
Aletheia.query("member(X, [a, b, c])", Episteme.Database.new())
#=> {:ok, [%{"X" => :a}, %{"X" => :b}, %{"X" => :c}]}See ALETHEIA.md#lists for the full v0.1 list
predicate family.
Where next
- Reference — every implemented predicate, in full, with signatures and semantics.
- Cheatsheet — the same ground, condensed to a lookup table.
- Examples — complete worked programs,
runnable from
examples/. - ../TUTORIAL.md — embedding Aletheia into an Elixir application: adding the dependency, building a database, running the REPL.