Registering a Python function¶
Sometimes a predicate or classifier is much easier to express in Python than in CLIPS —
regex matching, set overlap, temporal math, or an external lookup. Fathom lets you
register a plain Python callable once, then invoke it from the left-hand side of any
rule through the raw-CLIPS test: escape hatch on a ConditionEntry.
Register the function¶
Call Engine.register_function(name, callable). The callable becomes invokable from
CLIPS as (name arg1 arg2 ...).
from fathom.engine import Engine
engine = Engine()
engine.register_function("overlaps", lambda a, b: bool(set(a) & set(b)))
A few constraints worth knowing up front (see src/fathom/engine.py):
namemust be non-empty and match the regex[A-Za-z][A-Za-z0-9_-]*.namemust not start with the reservedfathom-prefix — that namespace is reserved for builtins the engine registers itself.- The callable takes positional arguments only.
- Re-registering an existing name overwrites the prior binding. This matches clipspy's semantics and is documented behaviour, not an error.
Call it from a rule¶
ConditionEntry.test (defined in src/fathom/models.py) is the escape hatch for
calling user-registered functions from a rule's LHS. It emits a raw (test <expr>)
conditional element verbatim, so Fathom's operator allow-list does not apply — you
get the full CLIPS expression language, including any function you registered.
The value of test must be a parenthesized CLIPS expression. The Pydantic validator
rejects anything that does not both start with ( and end with ).
ruleset: demo
module: MAIN
rules:
- name: route-shared-tag
when:
- template: request
conditions:
- slot: tags
bind: ?req_tags
- template: allowed
conditions:
- slot: tags
bind: ?allowed_tags
- test: "(overlaps ?req_tags ?allowed_tags)"
then:
action: allow
The (fn-name ?arg1 ?arg2) form is the convention: the function name comes first and
the arguments follow, all inside a single pair of parentheses. Bindings established
earlier in the when: block (here ?req_tags and ?allowed_tags) are in scope for
the test expression.
test may appear standalone in a ConditionEntry (no slot, expression, or
bind), in which case the pattern emits only the test CE. Combined with a slot
constraint, as above, both are emitted on the LHS.
Name restrictions¶
register_function raises ValueError at registration time when the name violates
any of these rules:
- The name must be non-empty.
- The name must match
[A-Za-z][A-Za-z0-9_-]*— an ASCII letter followed by letters, digits, underscores, or hyphens. - The name must not start with the reserved
fathom-prefix.
All three errors are raised synchronously by register_function before the callable
is handed to the underlying CLIPS environment, so misnamed registrations fail fast.
When not to use this¶
Prefer the built-in YAML operators whenever the check can be expressed as a slot
comparison. A condition like expression: equals(critical) keeps the logic in YAML,
stays inside Fathom's operator allow-list, and is visible to static tooling.
Reach for register_function plus a test: clause only when you need Python that
CLIPS cannot express directly: regex matching, set membership or overlap, arithmetic
on timestamps, or calling out to an external service. Every custom function widens
your rule surface beyond what the allow-list can vet, so use it deliberately and
keep each registered callable small, pure, and deterministic.