Clausal 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, 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 syntax is Python. Every .clausal file is valid Python syntax — no
new parser, no foreign notation. Your editor's syntax highlighting, linting,
and autocompletion work out of the box.
Data types are Python. Numbers are Python numbers. Lists are Python lists. Dicts are Python dicts. Strings remain strings. Declared atoms (symbolic constants) are lightweight classes with identity semantics. There is no marshalling, no conversion, no foreign data model.
The runtime is Python. Clausal runs on the Python VM. You can call any
Python library from within a logic predicate using ++(), and call logic
predicates from Python by iterating directly over predicate terms.
Import works as expected. from my_module import my_predicate loads
my_module.clausal through Python's import system. Bytecode is cached in
__pycache__ like any other Python module.
What's different¶
Variables are unknowns, not containers¶
in_ Python, a variable holds a value:
in_ Clausal, 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 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 predicate can produce multiple answers by having multiple clauses or through nondeterministic search:
# From Python, iterate over all answers:
from clausal import Var
from my_module import color
for trail in color(X := Var()):
print(X.value) # 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'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
with identity. when you declare atoms in -private or -module, Clausal
creates zero-arity classes:
From the Python side, red, green, and blue are class objects (not
strings). They have identity: red is red and red is not blue. Calling
them returns themselves: red() is red.
# From Python:
from my_module import red, green, Color
red is red # True — identity, not equality
red() is red # True — zero-arity call returns the class itself
isinstance(red, type) # True — it's a class
You can also create atoms dynamically:
from clausal.logic.predicate import make_atom, is_atom
ok = make_atom("ok")
err = make_atom("err")
is_atom(ok) # True
ok() is ok # True
Strings still work as data. String literals like "hello" flow through
unification as-is. The distinction is: declared atoms have identity (checked
with is), while strings have value equality (checked with ==). Use atoms
for symbolic constants (colors, states, tags); use strings for text data.
| Type check | What it tests |
|---|---|
atom(X) |
Declared atom (zero-arity class) |
is_str(X) |
Python string |
callable_(X) |
Atom, string, or compound term |
Mapping Python patterns to Clausal¶
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 Clausal 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, you describe the constraints and let CLP(ℤ) search:
-import_from(clpfd, [all_different, labeling]),
send_more_money([S, E, N, D, M, O, R, Y]) <- (
[S, E, N, D, M, O, R, Y] ins 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,
labeling([leftmost], [S, E, N, D, M, O, R, Y])
)
Parsing with grammars¶
DCGs (Definite Clause Grammars) let you describe grammars declaratively — and the same grammar can parse, generate, and validate:
Transparent integration with Python¶
You never leave the Python ecosystem. Call pandas, numpy, scikit-learn, or any Python library from within your logic predicates:
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 step by step.
See also: Python Integration — the ++() escape
and query API.
See also: Thinking Relationally — the mindset behind logic programming.