Clausal Prolog for Prolog Programmers¶
You know Prolog. You think in relations, you read clauses declaratively, and you reach for the most general query to understand a predicate. This page tells you what's the same, what's different, and where Python comes in.
The short version: Clausal Prolog (.clausal files) is Prolog. It uses
ISO syntax and aims for ISO Prolog conformity, so you write ordinary Prolog.
What it leaves out is cut. See Clausal Prolog for the
reference.
The philosophy is the same¶
Clausal Prolog is built on the same foundations you know:
- Predicates define relations. Clauses state conditions under which relations hold. Facts are unconditionally true. Rules have bodies.
- Unification is bidirectional. Variables on either side can be bound.
- Search is via backtracking. Multiple clauses are logical alternatives.
- Purity matters. Clausal Prolog has
dif/2, CLP(ℤ), CLP(B), CLP(ℝ), reified if-then-else, and tabling — all the tools for staying in the pure monotonic core. - No cut. Clausal Prolog does not have
!/0, by design.
The intellectual debt to Markus Triska's "The Power of Prolog" and Ulrich Neumerkel's work on purity is explicit and pervasive.
What is the same¶
Most of it. A .clausal file is read by an ISO Prolog front end:
- ISO syntax:
Head :- Body.,%and/* */comments, operators,TitleCasevariables,_and_Name,[H|T]lists, quoted atoms. - ISO builtin names, with ISO arithmetic:
X is N - 1evaluates and=unifies. - ISO error terms in Scryer's form,
error(Formal, Context). - Modules:
:- module(Name, Exports).,:- use_module(library(L)),:- use_module(M, [p/2]), and qualified goalsM:G. - DCGs:
-->withphrase/2,3. - Tabling:
:- table(p/2). - Libraries:
library(clpz),library(reif),library(lists),dif/2. - Double quotes:
"..."is a string, a list of characters, as in Scryer and Trealla.
Where ISO 13211-1 speaks, Clausal Prolog follows it. Where ISO is silent, it follows Scryer Prolog (not SWI).
Tabling¶
% graph.clausal
:- module(graph, [path/2]).
:- table(path/2).
edge(1, 2).
edge(2, 3).
edge(3, 1).
path(X, Y) :- path(X, Z), edge(Z, Y). % left recursion: fine when tabled
path(X, Y) :- edge(X, Y).
test("path terminates on the cycle") :-
findall(Y, path(1, Y), Ys), msort(Ys, [1, 2, 3]).
:- end_module(graph).
Same semantics as SWI's or XSB's tabling. Write the
directive with parentheses, :- table(path/2).: table is not a prefix
operator here, so :- table path/2. is a syntax error.
A test/1 (or test/2) clause is a unit test, run by
python -m clausal.testing graph.clausal or by pytest (see
Testing).
CLP(ℤ)¶
Prefer CLP(ℤ) to is/2 for integer arithmetic, as Triska recommends: it
works in every direction.
% queens.clausal
:- module(queens, [n_queens/2]).
:- use_module(library(clpz)).
n_queens(N, Qs) :-
length(Qs, N),
Qs ins 1..N,
safe_queens(Qs),
labeling([ff], Qs).
safe_queens([]).
safe_queens([Q|Qs]) :- safe_queens(Qs, Q, 1), safe_queens(Qs).
safe_queens([], _, _).
safe_queens([Q|Qs], Q0, D0) :-
Q0 #\= Q,
abs(Q0 - Q) #\= D0,
D1 #= D0 + 1,
safe_queens(Qs, Q0, D1).
test("the first 8-queens placement") :-
once(n_queens(8, Qs)), Qs = [1, 5, 8, 6, 3, 7, 2, 4].
:- end_module(queens).
This is the program from "The Power of Prolog", unchanged. See Constraints.
DCGs and strings¶
% greet.clausal
:- module(greet, [greeting//0]).
greeting --> "hello ", name.
name --> "world".
name --> "prolog".
test("parse") :- phrase(greeting, "hello prolog").
test("generate") :-
findall(S, phrase(greeting, S), Ss),
Ss = ["hello world", "hello prolog"].
test("a string is a list of characters") :- "ab" = [a, b].
:- end_module(greet).
"..." is a list of characters (double_quotes is chars, the default in
Scryer and Trealla), so a string literal is a DCG terminal. See
DCGs and Atoms vs strings.
Errors and the database¶
% errs.clausal
:- module(errs, []).
colour(red).
test("ISO error terms, in Scryer's form") :-
catch(atom_length(1, _), error(E, C), true),
E == type_error(atom, 1), C == atom_length/2.
test("assertz creates a dynamic procedure") :-
assertz(seen(x)), seen(x).
test("a static procedure cannot be modified") :-
catch(assertz(colour(blue)), error(E, _), true),
E == permission_error(modify, static_procedure, colour/1).
test("integer division truncates toward zero") :- X is -7 // 2, X == -3.
:- end_module(errs).
An uncaught error prints the term first, as Scryer does:
Uncaught logic exception: error(type_error(atom,1),atom_length/2). See
Exceptions and
assert_creates_dynamic.
Meta-predicates¶
findall/3, bagof/3, setof/3, forall/2,
\+/1, once/1 and call/1..8 are builtins.
Modules¶
A predicate sees the names of its own module, never the caller's. A
library that takes a goal argument declares it with meta_predicate/1, and
the goal is then resolved in the caller, as in Scryer:
% lib.clausal
:- module(lib, [all_hold/2]).
:- meta_predicate(all_hold(1, ?)).
all_hold(G, Xs) :- maplist(G, Xs).
:- end_module(lib).
% main.clausal
:- module(main, [small_list/0]).
:- use_module(lib, [all_hold/2]).
small(X) :- X < 10.
small_list :- all_hold(small, [1, 2, 3]).
:- end_module(main).
Without the meta_predicate declaration, small is looked up in lib and
the call raises existence_error(procedure, small/1); main:small names
it explicitly. There is no flat global predicate database. See
Name resolution is lexical, not dynamic.
What is different¶
No cut, by design¶
!, -> (if-then and if-then-else) and *-> (soft cut) are refused when
the file loads, with an error naming the file and line:
c.clausal:2: `!` (cut) is refused: Clausal is cut-free with no committed
choice, by design (ruling): !, -> and *-> are refused
(*-> is not an operator here, so it is a syntax error.) A ! built at
run time and passed to call/1 raises an error instead of running. The
design philosophy is that cut destroys monotonicity, separability, and
multi-directional use — all the properties that make logic programming
worthwhile.
Where you would use a cut or ->, Clausal Prolog offers:
- Reified if-then-else —
if_/3and the reified predicates oflibrary(reif)(=/3,memberd_t/3,tfilter/3, …) - CLP(ℤ) and dif/2 — replace cut-based pruning with constraints
once/1— when you want the first solution and say so- First-argument indexing — automatic, so the green cuts that only removed a choice point are unnecessary
% pure.clausal
:- module(pure, [max_of/3, sign/2, first_member/2]).
:- use_module(library(clpz)).
:- use_module(library(reif)).
% max(X, Y, X) :- X >= Y, !. max(_, Y, Y).
max_of(X, Y, Z) :- Z #= max(X, Y).
% sign(X, S) :- ( X =:= 0 -> S = zero ; S = nonzero ).
sign(X, S) :- if_(X = 0, S = zero, ( dif(X, 0), S = nonzero )).
% first_member(X, Xs) :- member(X, Xs), !.
first_member(X, Xs) :- once(member(X, Xs)).
test("max_of") :- max_of(3, 7, 7), max_of(7, 3, 7).
test("sign") :- sign(0, zero), sign(4, nonzero).
test("sign general") :- findall(X-S, sign(X, S), [0-zero, _-nonzero]).
test("first_member") :- first_member(X, [a, b]), X == a.
:- end_module(pure).
The if_/3 version of sign/2 answers the most general query
sign(X, S) correctly; the -> version would not.
Modules end with end_module/1¶
A module file must end with :- end_module(Name)., as every example on
this page does. Only comments and layout may follow it. A missing directive
is the ISO error error(existence_error(directive, end_module(Name)), load/1).
The flag require_end_module turns the requirement off for one file
(:- set_prolog_flag(require_end_module, false).) or for the process. See
end_module.
Scryer does not accept end_module/1: a file meant for Scryer as well
leaves it out (with the flag set to false), or has it removed with
clausal.end_module.strip_end_module.
No importing .pl modules¶
A .clausal module may not import a .pl module, because ISO Prolog may
use cut. :- use_module(oldcode, ...) on an oldcode.pl is refused at load
with permission_error(access, prolog_module, oldcode), and so are the
run-time routes (M:G, call/N of a qualified goal). The dependency is
one-way: a .pl module may import a .clausal one. To reuse .pl code,
convert it to .clausal, or wrap it in a .seam module.
Python only through library(...) and .seam modules¶
There is no ++expr or other Python escape in Clausal Prolog. It reaches
Python only through:
library(...)facades over the engine's Python modules (library(datetime),library(json),library(units), …), loaded like any Scryer library;- Python-free
.seammodules: Clausal code written in seam syntax, checked transitively; - Python bridges:
.seammodules that do run Python, listed under[tool.clausal] python_bridgesin the nearestpyproject.tomlabove the importing.clausalfile.
% app.clausal
:- module(app, [due/1, quadruple/2]).
:- use_module(library(datetime), [date_add/3, timedelta/3]). % a facade
:- use_module(helpers, [double/2]). % Python-free helpers.seam
due(D) :- timedelta(30, 0, TD), date_add(date(2026, 1, 15), TD, D).
quadruple(X, Y) :- double(X, Z), double(Z, Y).
:- end_module(app).
Anything else is refused at load: a .seam module that runs Python without
being listed raises permission_error(import, python_bridge, M), and
:- use_module(py/datetime, ...) raises
permission_error(access, python_module, py.datetime), naming the facade to
use instead. See
Python bridges.
Python calls Prolog¶
The other direction is open: any Python code can query a .clausal
module. After import clausal, import family loads family.clausal. In a
.seam file, a goal in for position is the toplevel's ?-:
# ask.seam
-import_from(family, [grandparent])
-private([tom])
for GRANDCHILD in --grandparent(tom, GRANDCHILD):
print(GRANDCHILD)
Answers are the engine's own terms: an atom is a Python str, a compound
term is a tuple ('f', 1, 2). From a plain .py file, use
clausal.call or clausal.solve. See
Python Integration.
Compilation, not interpretation¶
Clausal Prolog compiles predicates to Python generator functions at import time. There is no WAM and no interpreter loop:
- Bytecode is cached in
__pycache__ - First-argument indexing is computed at compile time
- Groundness-keyed dispatch generates specialized code paths
Existing .pl code¶
A .pl file is regular ISO Prolog, cut included. You have two ways to run
it.
On a real ISO engine. The Scryer and Trealla embeddings run unrestricted ISO Prolog in-process, cut and all, and you query them from Python with lazy iteration.
On the native engine, by importing it. Place it on sys.path and
import it like a module:
Experimental in 1.0
The in-process .pl loader is experimental: what it accepts and how it
names things may change in a minor release (see
Public API). It does not run cut yet: ! and -> in
a .pl file are refused when it loads, as they are in .clausal.
That is a current limitation of this loader, not a property of .pl.
Cross-file use_module between .pl files works, and a .pl file may
import a .clausal module. In a .pl file end_module/1 is optional.
If the same directory holds name.clausal and name.pl, the
.clausal file is loaded. See Importing Prolog Code
for the full guide.
A reminder about relational thinking¶
Even experienced Prolog programmers sometimes drift into procedural habits. Clausal Prolog's documentation is written to reinforce relational thinking throughout:
- We say predicates describe relations, not compute results
- We say clauses hold when conditions are met, not that they "match" or "execute"
- We encourage the most general query as a diagnostic
- We prefer relational names (nouns describing arguments) over imperative names (verbs describing actions)
If you've read Triska's "The Power of Prolog" or studied with Neumerkel, this will feel natural. If not, Thinking Relationally and Purity and Monotonicity lay out these ideas explicitly.
The seam: the Python-syntax boundary¶
The engine reads a third surface, the seam (.seam). It is Python
syntax, and it is where Python escapes (++expr), hosted Python
statements and the adapters over Python libraries live. Many other pages of
these docs show their examples in seam syntax. The semantics are shared;
the spellings differ:
| ISO / Clausal Prolog | Seam (.seam) |
Notes |
|---|---|---|
parent(tom, bob). |
parent(tom, bob), |
Trailing comma, not period. Bare atoms must be declared (-module/-private) or quoted. |
head :- a, b. |
head <- (a, b) |
|
X, Parent |
X, PARENT, Parent |
ALL_CAPS is the native style |
X = Y |
X is Y |
In the seam is unifies |
X is E, X #= E |
X == E |
In the seam == evaluates (CLP(ℤ) equality) |
X =\= Y, X #\= Y |
X != Y |
# starts a Python comment |
\+ G |
not G |
|
dif(X, Y) |
X is not Y or dif(X, Y) |
|
a --> b, c. |
a >> (b, c) |
|
:- module(m, [p/1]). … :- end_module(m). |
-module(m, [p(X)]) |
No end_module in the seam |
:- use_module(lib, [p/1]). |
-import_from(lib, [p]) |
|
:- table(p/2). |
-table(p/2) |
|
M:p(X) |
m.p(X) |
|
?- goal. |
for X in --goal(X): |
In a .seam file |
| — | ++expr |
Python; not available in Clausal Prolog |
See Clausal Prolog for the side-by-side reference and Syntax for the seam grammar.
Getting started¶
- Read Clausal Prolog for the surface and its rules
- Browse the Predicate Index for the builtins
- Look at Examples for N-Queens, Sudoku, map colouring, and more
See also: Importing Prolog Code — .pl import,
end_module, library(...) facades and Python bridges.
See also: Prolog Translation — translation between seam and Prolog syntax.
See also: Scryer Prolog Embedding — an in-process ISO Prolog engine for programs that need cut.
See also: Thinking Relationally — the mindset behind good logic programming.