Examples: the Aletheia language

Copy Markdown View Source

Complete, runnable .alp programs, showing the language itself rather than the Elixir embedding API — for that side, see ../EXAMPLES.md. The source for each lives under examples/ in this repository (linked per-file below); every query below was run against that exact source and its output pasted in verbatim.

{:ok, db} = Aletheia.consult("examples/family.alp")

Family tree (examples/family.alp)

Facts, simple rules built on them, and one recursive rule (ancestor/2):

Aletheia.query("parent(tom, X)", db)
#=> {:ok, [%{"X" => :bob}, %{"X" => :liz}]}

Aletheia.query("father(X, Y)", db)
#=> {:ok, [%{"X" => :tom, "Y" => :bob}, %{"X" => :tom, "Y" => :liz},
#          %{"X" => :bob, "Y" => :ann}, %{"X" => :bob, "Y" => :pat}]}

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}]}

Aletheia.query("sibling(ann, Y)", db)
#=> {:ok, [%{"Y" => :pat}]}

father/2 and mother/2 show rules layering a type-check (male/1/ female/1) on top of a plain fact; ancestor/2 shows genuine recursion through parent/2 — the second clause calls ancestor/2 from inside its own body, and the engine's real SLD-resolution loop handles it exactly like any other Prolog.

List processing (examples/lists.alp)

Ordinary recursive list predicates, on top of the v0.1 builtins:

{:ok, db} = Aletheia.consult("examples/lists.alp")

Aletheia.query("sum([1,2,3,4], S)", db)
#=> {:ok, [%{"S" => 10}]}

Aletheia.query("max_of([3,7,2,9,4], M)", db)
#=> {:ok, [%{"M" => 9}]}

Aletheia.query("double_all([1,2,3], D)", db)
#=> {:ok, [%{"D" => [2, 4, 6]}]}

Aletheia.query("count(a, [a,b,a,c,a], N)", db)
#=> {:ok, [%{"N" => 3}]}

count/3 is worth a second look — its middle clause has a cut right after matching the element being counted:

count(_, [], 0).
count(X, [X|T], N) :- !, count(X, T, N0), N is N0 + 1.
count(X, [_|T], N) :- count(X, T, N).

Without the !, a call like count(a, [a|T], N) could also (wrongly) match the third clause — [X|T] unifying X with the head of the list doesn't prevent [_|T] from also matching the same list. The cut commits to "this element matched" the moment it has, so the third clause is never even attempted for that position.

Cut and control flow (examples/cut_control.alp)

Three ways to write "classify by ranges," side by side, plus a direct cut-vs-once/1 comparison:

{:ok, db} = Aletheia.consult("examples/cut_control.alp")

Aletheia.query("classify(-5, R)", db)  #=> {:ok, [%{"R" => :negative}]}
Aletheia.query("classify(0, R)", db)   #=> {:ok, [%{"R" => :zero}]}
Aletheia.query("classify(5, R)", db)   #=> {:ok, [%{"R" => :positive}]}

Aletheia.query("sign(-3, R)", db)      #=> {:ok, [%{"R" => :negative}]}
Aletheia.query("max(3, 7, M)", db)     #=> {:ok, [%{"M" => 7}]}
Aletheia.query("max(7, 3, M)", db)     #=> {:ok, [%{"M" => 7}]}

classify/2 uses cut + clause order (the classic Prolog idiom); sign/2 gets the same result from a single clause with nested if-then-else; max/3 shows why the cut in max(X, Y, X) :- X >= Y, !. matters — the head alone would still let max(3, 7, M) try to unify M with the first argument on backtracking if nothing stopped it.

The cut/once/1 distinction, made concrete:

picked_cut(a) :- !.
picked_cut(b).

picked_once(a) :- once(true).
picked_once(b).
Aletheia.query("picked_cut(X)", db)   #=> {:ok, [%{"X" => :a}]}
Aletheia.query("picked_once(X)", db)  #=> {:ok, [%{"X" => :a}, %{"X" => :b}]}

Both clauses look similar, but ! and once/1 are not interchangeable: cut commits to the clause it appears in, so picked_cut/1's second clause is never tried; once/1 only limits the solution count of the goal it wraps, so it has no effect at all on whether picked_once/1's second clause gets tried — both a and b come back. See ALETHEIA.md#cut for the complete rules.

Exception handling (examples/exceptions.alp)

Two different guard styles, plus recovering from a thrown error:

{:ok, db} = Aletheia.consult("examples/exceptions.alp")

Aletheia.query("safe_div(10, 2, R)", db)    #=> {:ok, [%{"R" => 5}]}
Aletheia.query("safe_div(10, 0, R)", db)    #=> {:ok, [%{"R" => :undefined}]}
Aletheia.query("safe_div(10, foo, R)", db)  #=> {:ok, [%{"R" => :undefined}]}

Aletheia.query("safe_head([], H)", db)         #=> {:ok, [%{"H" => :none}]}
Aletheia.query("safe_head([1,2,3], H)", db)    #=> {:ok, [%{"H" => 1}]}

Aletheia.query("safe_eval(2 + 3, R)", db)  #=> {:ok, [%{"R" => 5}]}
Aletheia.query("safe_eval(foo, R)", db)    #=> {:ok, [%{"R" => :undefined}]}

safe_div/3's first clause guards before attempting arithmetic (\+ number(Y), !) — for a non-numeric divisor, which would otherwise raise a type_error(evaluable, _) the moment is/2 tried to evaluate it. Its second clause instead lets the (numeric) division attempt run and recovers from whatever it throws via catch/3 — division by zero raises a domain_error, which error(_, _)'s wildcard pattern catches just as well. safe_eval/2 shows the general "try, catch a specific shape, otherwise pass the real value through" pattern:

safe_eval(Expr, R) :-
  catch(R0 is Expr, error(type_error(evaluable, _), _), R0 = caught),
  (R0 == caught -> R = undefined ; R = R0).

R0 is either the real evaluated result or the sentinel atom caught (if catch/3's recovery goal ran); the -> then picks R accordingly.

Where next

  • Reference for the complete predicate list this built on.
  • Cheatsheet for a condensed lookup once you've internalized the above.
  • ../EXAMPLES.md for embedding examples (the Elixir side of running these).