Prolog invented logic programming and powered a generation of expert systems. Its ideas — facts,
rules, backward-chaining inference — are timeless, and it has been a stable ISO standard
(ISO/IEC 13211-1) since 1995. Its ergonomics are another matter: you hand-roll the
query-and-format plumbing, conflict resolution is manual, side effects mean assert/retract,
and there’s no built-in way to test a ruleset, learn a threshold, or call an external tool.
Tln keeps the logic and modernizes everything around it. These pages put the two side by side
using real programs from the Tln examples. The
Prolog is idiomatic ISO Prolog (ISO/IEC 13211-1:1995 or later), syntax-checked with
SWI-Prolog; the Tln is the actual .tln source, validated with the tln compiler.
Both reason over the same external facts, loaded as generic triples:
record(Entity, Id, Type, Category, Status, Date)
attr(Entity, Id, Name, Value)
Take the classic family tree. In Prolog you write the facts, the rules, and the query-and-print
plumbing to use them. In Tln the parent links arrive as facts from your systems — never written
in .tln — so a relationship is just a rule that declares its own output:
ISO Prolog (1995)
% facts, rules, AND the query-and-print plumbing:
parent(tom, bob).
parent(bob, ann).
parent(bob, pat).
grandparent(X, Z) :- parent(X, Y), parent(Y, Z).
ancestor(X, Z) :- parent(X, Z).
ancestor(X, Z) :- parent(X, Y), ancestor(Y, Z).
list_children(P) :-
forall(parent(P, C),
format("~w~n", [C])).
Tln
// Facts arrive from your systems as record/attr triples
// (loaded, never written in .tln):
// record(family, "bob", person) attr(family, "bob", "parent", "tom")
// record(family, "ann", person) attr(family, "ann", "parent", "bob")
// record(family, "pat", person) attr(family, "pat", "parent", "bob")
detect "Children of Tom" {
for records where type == "person"
and attr "parent" == "tom"
flag matching items
label "{item.name} is a child of Tom"
}
Fewer moving parts, and the outcome is declared rather than scripted. Explore the full worked
comparisons below.
And it isn’t only rewrite-by-hand: existing Prolog can be ported. The relational subset lowers
to native Tln rules — even recursive ones with comparison/threshold guards — while the
genuinely Prolog-only parts (compound terms, cut, assert, value-inventing arithmetic) keep
running on the pure-Go tln-prolog engine — no external
Prolog required.
An LLM extracts facts from each claim invoice (PDF → record + attrs). The expert system then decides — deterministically — whether to auto-approve, auto-reject, or escalate to a human. No LLM in the decision loop; every outcome traces to a specific rule.
Block a blacklisted provider A claim from a blacklisted provider must never be approved — and this decision must win over any “auto-approve” that also matches. In Prolog you model the decision term and its message yourself; in Tln you block with a reason and a CRITICAL priority that resolves the conflict for you.
...
This is the comparison closest to Prolog’s heart: derived predicates and recursive rules. Tln keeps the Datalog surface Prolog programmers already know — and fixes the one place ISO Prolog’s negation quietly breaks.
Derived predicates A derive block names a boolean predicate over a record; any other block references it as pred(v), exactly like an asserted fact. It’s the same idea as a Prolog rule head — but the planner inlines it, and the blocks that use it stay declarative (flag / label instead of a forall/format loop). Both programs below are real: the Prolog is ISO-standard and SWI-checked, the Tln is examples/vehicle_recall.tln.
...
Vehicle service tracking from examples/fleet_maintenance.tln: flag active vehicles overdue for service, then forecast a parts stock-out.
Service overdue Two named conditions compose into a detection. In Prolog these are rule heads and a manual report predicate; in Tln they’re defines referenced with is, feeding a declarative detect.
ISO Prolog (1995)active_vehicle(E, Id) :- record(E, Id, item, 'Vehicles', active, _). overdue_km(E, Id) :- attr(E, Id, km, Km), attr(E, Id, last_service_km, Last), Km > Last. service_overdue(E, Id) :- active_vehicle(E, Id), overdue_km(E, Id). report_overdue(E) :- forall(service_overdue(E, Id), ( attr(E, Id, name, Name), attr(E, Id, km, Km), attr(E, Id, last_service_km, Last), format("~w: ~w km since last service at ~w km~n", [Name, Km, Last]) )). Tlndefine "active_vehicle" { type == "item" and status == "active" and category == "Vehicles" } define "overdue_km" { attr "km" > attr "last_service_km" } detect "Service overdue" { for records where is "active_vehicle" and is "overdue_km" flag matching items label "{item.name}: {attr.km} km since last service at {attr.last_service_km} km" } A forecast Prolog can’t express The same file then predicts when a part will run out — a time-series forecast over the last 90 days of stock levels:
...