Clausal Prolog for Python Programmers¶
You know Python. You know functions, loops, classes, list comprehensions. This page bridges that knowledge to logic programming — what's different, what maps to what, and why you'd want to use it.
The thirty-second version¶
In Python, you write functions that compute results from inputs. In Clausal Prolog, you write relations that describe when something is true about their arguments. A relation has no fixed inputs or outputs — the same definition can compute, verify, generate, and enumerate.
# A relation between a list, a prefix, and a suffix
append([], SUFFIX, SUFFIX),
append([HEAD, *TAIL], SUFFIX, [HEAD, *REST]) <- (
append(TAIL, SUFFIX, REST)
)
This single definition can concatenate two lists, split a list into all possible prefix/suffix pairs, verify that three lists are related, or generate completions from partial information. No separate functions needed.
What stays the same¶
The seam's syntax is Python. Logic code that lives next to Python is
written in the seam (.seam), and every .seam file is valid Python syntax —
no new parser, no foreign notation. Your editor's syntax highlighting, linting,
and autocompletion work out of the box. The examples on this page use the seam.
The main language, Clausal Prolog (.clausal), is
written in ISO Prolog syntax instead.
Data types are mostly Python. Numbers are Python numbers. Lists are Python
lists. An atom (a symbolic constant) is a Python str — no wrapper class.
A compound term is a plain tuple, ('point', 1, 2). A logic string (what
"…" means) is the carrier ('$chars', text), a different value from the
atom of the same spelling — see
Atoms are symbolic constants below and
Terms are tuples.
The runtime is Python. Clausal Prolog runs on the Python VM. You can call any
Python library from within a seam predicate using ++(), and ask a
question from Python by writing the goal after -- in a .seam file:
# ask.seam — Python that can also speak Clausal terms
-import_from(my_module, [color])
for X in --color(X): # every answer; X is an ordinary Python local
print(X)
From a plain .py file the lower-level API does the same:
solve(("color", X := Var()), module=my_module) — a goal is a tuple, run
against the module that defines it. See
Python Integration.
Import works as expected. import my_module loads
my_module.seam (or my_module.clausal, my_module.pl) through Python's import system. Bytecode is cached in
__pycache__ like any other Python module. The module's attribute for a
predicate is that predicate's handle, used to name it — not a function to
call.
What's different¶
Variables are unknowns, not containers¶
In Python, a variable holds a value:
In Clausal Prolog, a logic variable is an unknown — it starts unbound and gets bound through unification. once bound, it cannot be reassigned (within that branch of search). Logic variables are written in ALLCAPS:
This is closer to variables in algebra than variables in Python: X stands
for some value, and the system finds what that value must be.
No return values — relations hold or don't¶
A Python function returns a value. A Clausal Prolog predicate either holds (is true for the given arguments) or doesn't hold. Instead of returning results, you add an argument:
Multiple answers via backtracking¶
A Python function produces one result. A Clausal Prolog predicate can produce multiple answers by having multiple clauses or through nondeterministic search:
# ask.seam — iterate over all answers:
-import_from(my_module, [color])
for X in --color(X):
print(X) # red, green, blue
This replaces explicit loops and generators. Instead of writing code that searches, you describe what you're looking for and let the system search.
Pattern matching is bidirectional unification¶
Python 3.10+ has match statements, but they are one-directional: you match a
value against patterns. Clausal Prolog's unification is bidirectional — variables on
both sides can be bound:
This bidirectionality is what makes relations work in all directions.
Atoms are symbolic constants¶
In logic programming, an atom is a symbolic constant — like an enum
value. An atom is the interned Python str: declaring one in -private
or -module doesn't wrap it in a class, it just interns the spelling:
From the Python side, red, green, and blue are plain str objects.
Compare them with == — because atoms are interned, is happens to agree
too, but == is the test to write:
# From Python:
from my_module import red, blue
from clausal.logic.atoms import mint, is_atom, spelling
red # 'red'
red == mint("red") # True
red == blue # False
is_atom(red) # True
spelling(red) # 'red'
Every Python str is an atom — there is no wrapper to opt in to.
is_atom("ok") is True for any Python str, declared or not; a Python str
crossing into the engine (a to_term argument, a ++ result, a dict key) is
always read as the atom of that spelling. mint interns and hands back that
same value:
from clausal.logic.atoms import mint, is_atom
ok = mint("ok") # 'ok'
is_atom(ok) # True
is_atom("ok") # True — a raw str IS an atom
ok == "ok" # True — the same value
A 0-arity predicate — a procedure, a different thing from an atom — is
defined in a .seam module (ok, or ok <- ...) and is a row in that
module's database; the module binds its name to a predicate handle. A
predicate is never a Python class: define it in a .seam or .clausal module, or, for
a predicate implemented in Python, give a plain object a _get_dispatch()
method (see Public API).
Strings are the other kind — not a bare str. A "hello" literal is a
string, as in Scryer and Trealla: the carrier ('$chars', 'hello'), which the
engine treats as the list of its character atoms. (A module can still declare
the temporary -double_quotes(atom) setting
to read "…" as an atom.) Atoms and strings never unify: atom(hello) holds
for the atom but atom("hello") fails for the string. From Python, compare
a string answer against a --"hello" term, or convert it with
clausal.to_python, which gives the text 'hello'. Use atoms for symbolic
constants (colours, states, tags); use strings for text data.
| Type check | What it tests |
|---|---|
atom(X) |
An atom — a Python str |
string(X) / is_str(X) |
A string (the ('$chars', text) carrier) |
atomic(X) |
An atom or a number — not a string, which is a list |
callable_(X) |
An atom or a compound term — a string counts, being a non-empty list, as in Scryer |
Mapping Python patterns to Clausal Prolog¶
For-loops become recursive relations¶
# Clausal: relation between a list and its sum
list_sum([], 0),
list_sum([HEAD, *TAIL], TOTAL) <- (
list_sum(TAIL, SUBTOTAL),
TOTAL == SUBTOTAL + HEAD
)
Read it declaratively: "The sum of the empty list is 0. The sum of [HEAD, *TAIL] is TOTAL when the sum of TAIL is SUBTOTAL and TOTAL is SUBTOTAL + HEAD."
If/else becomes multiple clauses¶
# Python
def classify(n):
if n > 0: return "positive"
elif n == 0: return "zero"
else: return "negative"
# Clausal: three clauses, three cases
classify(N, 'positive') <- (N > 0)
classify(0, 'zero'),
classify(N, 'negative') <- (N < 0)
Each clause is a logical alternative — a separate condition under which the relation holds.
List comprehensions become search with meta-predicates¶
# Clausal: describe the relation, collect with findall
square_of_even(N, SQ) <- (
between(0, 9, N),
N % 2 == 0,
SQ == N * N
)
test("squares") <- (
findall(SQ, square_of_even(_, SQ), SQUARES),
SQUARES == [0, 4, 16, 36, 64]
)
Dictionaries become facts¶
# Clausal: facts that can be queried in any direction
capital('france', 'paris'),
capital('germany', 'berlin'),
capital('japan', 'tokyo'),
The relational version can be queried both ways: "What is the capital of France?" and "Which country has Paris as its capital?"
Why bother?¶
Constraint solving for free¶
Need to solve a Sudoku, schedule a timetable, or find valid configurations? In Python, you'd reach for a solver library or write custom search. In Clausal Prolog, you describe the constraints and let CLP(ℤ) search:
send_more_money([S, E, N, D, M, O, R, Y]) <- (
in_domain([S, E, N, D, M, O, R, Y], 0, 9),
all_different([S, E, N, D, M, O, R, Y]),
S != 0, M != 0,
1000*S + 100*E + 10*N + D
+ 1000*M + 100*O + 10*R + E
== 10000*M + 1000*O + 100*N + 10*E + Y,
label([S, E, N, D, M, O, R, Y])
)
--send_more_money(L) answers [9, 5, 6, 7, 1, 0, 8, 2].
Parsing with grammars¶
DCGs (Definite Clause Grammars) let you describe grammars declaratively — and the same grammar can parse, generate, and validate:
--phrase(greeting, X) generates ['hello', 'world'] and
['hello', 'clausal']; with X given, it checks a sentence.
Transparent integration with Python¶
You never leave the Python ecosystem. Call pandas, numpy, scikit-learn, or any Python library from within your logic predicates (in seam files):
Getting started¶
- Install:
pip install clausal - Read the Tutorial — it builds from simple facts to recursive relations
- Read Thinking Relationally — the mental shift that makes everything click
- Browse the Predicate Index for available builtins
- Try the Constraints for your first "wow" moment
See also: Tutorial — learn Clausal Prolog step by step.
See also: Python Integration — the -- and ++
seams and the query API.
See also: Thinking Relationally — the mindset behind logic programming.