PatLang Paradigm Gallery: Singles and Pairs
Every example below is a real, verified-running PatLang program (via pat --ir-run) — not a sketch. The first eight demonstrate one paradigm each in isolation; the fifteen after that combine two of the six "real" paradigms (imperative and procedural are implicit in almost any code, so they're not combined with anything here) to show how PatLang lets them coexist in one program rather than forcing a single style throughout. One triple is included too, added along the way rather than as part of a planned batch (the other 19 three-way combinations remain backlog).
Singles
Imperative
A sequence of statements that directly mutate state, step by step, in the order written. No functions, no objects, no closures — just "do this, then do that." Simple to trace line by line; scales poorly once the same logic is needed in more than one place, since there's nothing to name and reuse.
# Imperative: a sequence of statements that directly mutate state, # step by step, in the order written. No functions, no objects, no # closures -- just "do this, then do that." let total = 0 let i = 1 while i <= 5 do let total = total + i let i = i + 1 end print(total)
Output:
15
Procedural
The same kind of step-by-step logic as imperative code, but decomposed into named, reusable procedures. Each one does a single well-defined job and can be called by name rather than inlined — the natural next step once imperative code starts repeating itself.
# Procedural: the same kind of step-by-step logic as imperative code, # but decomposed into named, reusable procedures -- each one a single # well-defined piece of the overall computation, called by name rather # than inlined. make a function called square takes n returns r return n * n end make a function called sum_of_squares takes a, b returns r return square(a) + square(b) end print(sum_of_squares(3, 4))
Output:
25
Functional
Functions as first-class values. make_multiplier doesn't just compute a result — it RETURNS a closure that captures factor at creation time, ready to be called later with a different argument each time, with no shared mutable state anywhere. Two closures built from the same factory stay completely independent.
# Functional: functions as first-class values. make_multiplier doesn't
# just compute a result -- it RETURNS a closure that captures `factor`
# at creation time, ready to be called later with a different argument
# each time, with no shared mutable state anywhere.
make a function called make_multiplier takes factor returns closure
return |x| { return x * factor }
end
let triple = make_multiplier(3)
let double = make_multiplier(2)
print(triple(7))
print(double(7))
Output:
21
14
Object-Oriented
Named objects with mutable state accessed through get/send rather than passed around explicitly. new creates the object; send mutates a field; get reads it back — the object's own count survives between calls, unlike the pure functional style next door.
# Object-oriented: named objects with mutable state accessed through
# dot notation rather than passed around explicitly. `new` creates the
# object; `c1.count = ...` mutates a field on it; `c1.count` reads a
# field back -- the object's own count survives between calls, unlike
# the pure functional style next door.
let c1 = new("Counter", "c1")
c1.count = 0
c1.count = c1.count + 1
c1.count = c1.count + 1
print(c1.count)
Output:
2
Logic
Declare FACTS (not steps), then ask questions against them. Nothing here says HOW to search the facts — just what's true, and a query that asks "how many of these match?" A genuinely different way of expressing a program: as knowledge, not instructions.
# Logic: declare FACTS (not steps), then ask questions against them.
# Nothing here says HOW to search the facts -- just what's true, and a
# query that asks "how many of these match?"
fact("parent", "alice", "bob")
fact("parent", "alice", "carol")
fact("parent", "bob", "dana")
print(query("parent", "alice", 0))
Output:
2
Event-Driven
A handler declared once (when NAME { ... }), reacting to whatever gets emitted later, wherever that happens in the program. The handler never gets called directly by name — it fires whenever a matching event is raised, decoupling "what happens" from "who triggered it."
# Event: a handler declared once (`when NAME { ... }`), reacting to
# whatever gets `emit`ted later, wherever that happens in the program.
# The handler never gets CALLED directly by name -- it fires whenever
# a matching event is raised, decoupling "what happens" from "who
# triggered it."
when tick {
print("tick: " + event_data)
}
emit("tick", "1")
emit("tick", "2")
Output:
tick: 1
tick: 2
Goal-Oriented
Declare what you want (goal), not the steps to get there. pursue plans a sequence of registered actions that achieves it — a real search over preconditions/effects, not a fixed script — and activate then actually runs each planned action's bound closure in order.
# Goal-oriented: declare what you want (`goal`), not the steps to get
# there. `pursue` plans a sequence of registered actions that achieves
# it (a real search over preconditions/effects, not a fixed script);
# `activate` then actually runs each planned action's bound closure in
# order.
action_add("chop_wood", [], [["has_wood", "yard"]], [], 1)
action_bind("chop_wood", |args| {
print("chopping wood")
return true
})
goal get_wood {
has_wood(yard)
}
let p = pursue get_wood
print(activate p)
Output:
chopping wood
true
Custom Syntax (DSL)
A syntax block defines a small custom grammar (here, an HTTP route table) that reads nothing like ordinary PatLang — it's expanded, textually, into plain PatLang calls BEFORE the normal parser ever sees it, so the rest of the language doesn't need to know this notation exists at all.
# DSL: a `syntax` block defines a small custom grammar (here, an HTTP
# route table) that reads nothing like ordinary PatLang -- it's
# expanded, textually, into plain PatLang calls BEFORE the normal
# parser ever sees it, so the rest of the language doesn't need to know
# this notation exists at all.
syntax RouterDSL {
trigger: Keyword("routes");
tokens {
HttpVerb(verb) = regex("\b(GET|POST|PUT|DELETE)\b", verb);
UrlPath(path) = regex("\/[a-zA-Z0-9_\/:-]*", path);
Arrow = "->";
}
rule RouteLine {
let verb = expect HttpVerb;
let path = expect UrlPath;
expect Arrow;
let controller = expect Identifier;
expect Symbol(".");
let action = expect Identifier;
return AST.RegisterRoute(verb, path, controller, action);
}
}
routes {
GET /users -> UserController.index
POST /users/:id/edit -> UserController.update
}
print("routes registered")
Output:
routes registered
Pairs
Functional + OO
A closure factory (functional) that produces operations to apply to a mutable object's state (OO). make_adjuster returns a closure capturing delta; each returned closure, when called, mutates whichever account it's handed via send/get — the closure itself stays pure (same delta every time), while the OBJECT it operates on is where the mutable state actually lives.
# Functional + OO: a closure factory (functional) that produces
# operations to apply to a mutable object's state (OO). `make_adjuster`
# returns a closure capturing `delta`; each returned closure, when
# called, mutates whichever account it's handed via dot notation -- the
# closure itself stays pure (same delta every time), while the OBJECT
# it operates on is where the mutable state actually lives.
let a1 = new("Account", "a1")
a1.balance = 100
make a function called make_adjuster takes delta returns closure
return |acct| {
acct.balance = acct.balance + delta
return acct.balance
}
end
let deposit_10 = make_adjuster(10)
print(deposit_10("a1"))
print(deposit_10("a1"))
Output:
110
120
Functional + Logic
Facts store only raw data (each person's date of birth). Two ordinary functions — age_from_dob and classify — are genuinely PASSED AS VALUES into map_list/filter_list, chained one into the other, over a single query's results. Rule CHAINING (person_dob(X, D) :- person(X), dob(X, D).) collapses what would otherwise be two solve calls per person into one. solve itself still can't invoke a function during resolution — the same architectural split as GOAP's pursue (pure planning) vs activate (actual closure dispatch, synthesized by the lowerer): the logic engine resolves the pure part, ordinary PatLang code applies the functions afterward.
# Functional + logic: facts store only raw data (each person's date of
# birth). Two ordinary functions -- age_from_dob and classify -- are
# genuinely PASSED AS VALUES into map_list/filter_list, chained one into
# the other, and applied across a single logic-engine query's results.
#
# Rule CHAINING (a rule body naming more than one other predicate) really
# does simplify this: person_dob(X, D) :- person(X), dob(X, D). combines
# what would otherwise be two separate solve() calls per person (get the
# name, then separately look up their dob) into one.
#
# Two real limits confirmed while building this (not oversights -- the
# same architecture applies to goal/action: see host_action_bind's doc
# comment in hosts.rs):
# 1. `solve` itself cannot invoke a function during resolution -- rule
# bodies only unify (plus a special-cased `neq`); there is no path
# from the native solve_goal/resolve_conjuncts code back into the
# interpreter to call a stored PatLang closure. That's exactly why
# GOAP splits `pursue` (pure planning, no closures) from `activate`
# (the part that actually calls bound closures, synthesized as real
# control-flow by the lowerer) -- solve/map_list here is the same
# split: solve resolves the pure logical part, ordinary PatLang
# code (map_list/filter_list) applies the functions afterward.
# 2. a plain named function (`classify`) is not itself a first-class
# value -- it must be wrapped in a closure literal to be passed as
# data, same as elsewhere in this example.
make a function called age_from_dob takes birth_year returns years
return 2026 - birth_year
end
make a function called classify takes age returns label
if age < 18 then
return "minor"
end
return "adult"
end
make a function called map_list takes items, f returns out
let out = []
let i = 0
let n = to_num(list_len(items))
while i < n do
let out = list_push(out, f(items[i]))
let i = i + 1
end
return out
end
make a function called filter_list takes items, pred returns out
let out = []
let i = 0
let n = to_num(list_len(items))
while i < n do
if pred(items[i]) then
let out = list_push(out, items[i])
end
let i = i + 1
end
return out
end
rule person(alice).
rule dob(alice, 1996).
rule person(bob).
rule dob(bob, 2015).
rule person_dob(X, D) :- person(X), dob(X, D).
let pairs = solve("person_dob", ["X", "D"])
let classify_pair = |pair| { classify(age_from_dob(to_num(pair[1]))) }
print(map_list(pairs, classify_pair))
let name_of = |pair| { pair[0] }
let is_adult = |pair| { classify(age_from_dob(to_num(pair[1]))) == "adult" }
print(map_list(filter_list(pairs, is_adult), name_of))
Output:
[adult, minor]
[alice]
Functional + Event
A closure factory builds a reusable formatter; the event handler calls it whenever a "greet" event arrives, rather than hard-coding the greeting inline. The closure is built FRESH inside the handler, not captured from an outer let — referencing a top-level let from inside a function/handler is a known PatLang gotcha (see Capabilities & Honest Limitations).
# Functional + event: a closure factory builds a reusable formatter;
# the event handler calls it whenever a "greet" event arrives, rather
# than hard-coding the greeting inline. (The closure is built FRESH
# inside the handler, not captured from an outer `let` -- referencing
# a top-level `let` from inside a function/handler is a known PatLang
# gotcha, see patlang-capabilities.html.)
make a function called make_greeter takes greeting returns closure
return |name| { return greeting + ", " + name + "!" }
end
when greet {
let hello = make_greeter("Hello")
print(hello(event_data))
}
emit("greet", "Alice")
emit("greet", "Bob")
Output:
Hello, Alice!
Hello, Bob!
Functional + Goal-Oriented
action_bind is exactly where these two meet — a plan's step needs SOME code to actually run, and that code is just an ordinary closure, built here by a closure FACTORY so the same "make an action" logic can stamp out differently-labelled actions without repeating itself.
# Functional + goal-oriented: `action_bind` is exactly where these two
# meet -- a plan's step needs SOME code to actually run, and that code
# is just an ordinary closure, built here by a closure FACTORY so the
# same "make an action" logic can stamp out differently-labelled
# actions without repeating itself.
make a function called make_action takes label returns closure
return |args| {
print(label + ": building")
return true
}
end
action_add("build_house", [], [["built", "house"]], [], 1)
action_bind("build_house", make_action("build_house"))
goal have_house {
built(house)
}
let p = pursue have_house
print(activate p)
Output:
build_house: building
true
Functional + DSL
The custom route syntax handles registration; a closure factory (ordinary functional code, nothing DSL-specific about it) builds a human-readable description of one of the routes it just declared.
# Functional + DSL: the custom route syntax handles registration; a
# closure factory (ordinary functional code, nothing DSL-specific about
# it) builds a human-readable description of one of the routes it just
# declared.
syntax RouterDSL {
trigger: Keyword("routes");
tokens {
HttpVerb(verb) = regex("\b(GET|POST|PUT|DELETE)\b", verb);
UrlPath(path) = regex("\/[a-zA-Z0-9_\/:-]*", path);
Arrow = "->";
}
rule RouteLine {
let verb = expect HttpVerb;
let path = expect UrlPath;
expect Arrow;
let controller = expect Identifier;
expect Symbol(".");
let action = expect Identifier;
return AST.RegisterRoute(verb, path, controller, action);
}
}
routes {
GET /users -> UserController.index
}
make a function called describe_route takes verb, path returns closure
return |suffix| { return verb + " " + path + suffix }
end
let describe = describe_route("GET", "/users")
print(describe(" (registered via DSL)"))
Output:
GET /users (registered via DSL)
OO + Logic
An object holds a student's mutable grade; the logic engine independently tracks a fact about whether they've passed. Neither paradigm needs to know about the other's representation. The pass/fail state is folded into the PREDICATE name (passed_yes) rather than a plain passed predicate with the boolean as an argument — query's third argument is always a variable to bind, never a value to check equality against, so the naive form would print the same result regardless of what was actually recorded.
# OO + logic: an object holds a student's mutable grade; the logic
# engine independently tracks a fact about whether they've passed.
# Neither paradigm needs to know about the other's representation.
#
# Pass/fail is folded into the PREDICATE name (passed_yes, not a plain
# passed predicate with "true"/"false" as an argument) because query()'s
# third argument is always a variable to BIND, never a value to check
# equality against -- fact("passed", "s1", "false") would STILL make
# query("passed", "s1", 0) print "1", the same vacuous-example bug found
# and fixed in functional_logic.patlang/logic_event.patlang/
# logic_goal.patlang/logic_dsl.patlang. Predicate equality genuinely is
# checked, so this really does depend on which fact was asserted.
let s1 = new("Student", "s1")
s1.grade = 85
fact("passed_yes", "s1", "recorded")
print(query("passed_yes", "s1", 0))
print(s1.grade)
Output:
1
85
OO + Event
The event handler is where these two paradigms actually meet — increment doesn't know or care who raises it, but when it fires, it mutates a genuine, persistent object's state via send/get.
# OO + event: the event handler is where these two paradigms actually
# meet -- `increment` doesn't know or care who raises it, but when it
# fires, it mutates a genuine, persistent object's state via dot
# notation.
new("Counter", "c1")
let c1 = "c1"
c1.count = 0
when increment {
let c1 = "c1"
c1.count = c1.count + 1
}
emit("increment")
emit("increment")
emit("increment")
print(c1.count)
Output:
3
OO + Goal-Oriented
The planner decides WHAT to do and in what order; the bound action closure carries out the effect by mutating a real object's state, same as any other OO code would.
# OO + goal-oriented: the planner decides WHAT to do and in what
# order; the bound action closure carries out the effect by mutating a
# real object's state via dot notation, same as any other OO code
# would.
let w1 = new("Workshop", "w1")
w1.wood = 0
action_add("chop", [], [["has_wood", "yard"]], [], 1)
action_bind("chop", |args| {
w1.wood = w1.wood + 1
return true
})
goal get_wood {
has_wood(yard)
}
let p = pursue get_wood
activate p
print(w1.wood)
Output:
1
OO + DSL
The custom route syntax declares routes; an ordinary object independently tracks how many have been registered so far — two different ways of modelling state, coexisting in one program.
# OO + DSL: the custom route syntax declares routes; an ordinary
# object independently tracks how many have been registered so far
# via dot notation -- two different ways of modelling state,
# coexisting in one program.
syntax RouterDSL {
trigger: Keyword("routes");
tokens {
HttpVerb(verb) = regex("\b(GET|POST|PUT|DELETE)\b", verb);
UrlPath(path) = regex("\/[a-zA-Z0-9_\/:-]*", path);
Arrow = "->";
}
rule RouteLine {
let verb = expect HttpVerb;
let path = expect UrlPath;
expect Arrow;
let controller = expect Identifier;
expect Symbol(".");
let action = expect Identifier;
return AST.RegisterRoute(verb, path, controller, action);
}
}
let r1 = new("Router", "r1")
r1.route_count = 0
routes {
GET /users -> UserController.index
POST /users/:id/edit -> UserController.update
}
r1.route_count = 2
print(r1.route_count)
Output:
2
Logic + Event
An event carries a payload (a [name, age] pair); the handler turns that payload into a genuine fact the logic engine can be queried against afterward. The age is folded into the PREDICATE name (age_30) rather than a plain age predicate with the number as an argument, since query's third argument only ever binds a variable, never checks a value for equality — querying age_30 vs age_31 is what actually proves the recorded age matters.
# Logic + event: an event carries a payload (a [name, age] pair); the
# handler turns that payload into a genuine fact the logic engine can
# be queried against afterward.
#
# The age itself is folded into the PREDICATE name (age_30, not a plain
# "age" predicate with 30 as an argument) because query()'s third
# argument is always a variable to BIND, never a value to check equality
# against (logic_engine.rs's unify_terms always succeeds against a Var)
# -- a plain query("age", "alice", 0) would print "1" regardless of
# what age was actually recorded, the same vacuous-example bug found and
# fixed in functional_logic.patlang. Predicate equality, by contrast, IS
# a real check (fact.pred == query.pred), so querying "age_30" vs
# "age_31" genuinely depends on the payload's actual age value.
when record_age {
fact("age_" + event_data[1], event_data[0], "recorded")
}
emit("record_age", ["alice", 30])
print(query("age_30", "alice", 0))
print(query("age_31", "alice", 0))
Output:
1
0
Logic + Goal-Oriented
The planner's action body checks a fact (declarative knowledge) before doing its work, blending "what's true" with "what to do about it." Whether the axe is held is folded into the predicate name (has_axe_yes) for the same reason as Logic + Event -- query's var-slot argument can't check a specific value.
# Logic + goal-oriented: the planner's action body checks a fact
# (declarative knowledge) before doing its work, blending "what's
# true" with "what to do about it."
#
# The axe's yes/no state is folded into the PREDICATE name (has_axe_yes,
# not a plain has_axe predicate with "yes"/"no" as an argument) because
# query()'s third argument is always a variable to BIND, never a value
# to check equality against -- fact("has_axe", "player", "no") would
# STILL make query("has_axe", "player", 0) print "1", the same vacuous-
# example bug found and fixed in functional_logic.patlang/
# logic_event.patlang. Predicate equality genuinely is checked
# (fact.pred == query.pred), so this really does depend on which fact
# was asserted.
fact("has_axe_yes", "player", "recorded")
action_add("chop_wood", [], [["has_wood", "yard"]], [], 1)
action_bind("chop_wood", |args| {
print("chopping (has axe: " + query("has_axe_yes", "player", 0) + ")")
return true
})
goal get_wood {
has_wood(yard)
}
let p = pursue get_wood
print(activate p)
Output:
chopping (has axe: 1)
true
Logic + DSL
The custom route syntax handles the declaration; a plain fact independently records the same information so it can be queried the logic-engine way, without the DSL machinery knowing anything about facts at all. The route path is folded into the predicate name for the same reason as the other Logic pairs here.
# Logic + DSL: the custom route syntax handles the declaration; a
# plain fact independently records the same information so it can be
# queried the logic-engine way, without the DSL machinery knowing
# anything about facts at all.
#
# The route's path is folded into the PREDICATE name (route_/users, not
# a plain route predicate with "/users" as an argument) because query()'s
# third argument is always a variable to BIND, never a value to check
# equality against -- fact("route", "GET", "/wrong") would STILL make
# query("route", "GET", 0) print "1", the same vacuous-example bug found
# and fixed in functional_logic.patlang/logic_event.patlang/
# logic_goal.patlang. Predicate equality genuinely is checked, so this
# really does depend on which route was asserted.
syntax RouterDSL {
trigger: Keyword("routes");
tokens {
HttpVerb(verb) = regex("\b(GET|POST|PUT|DELETE)\b", verb);
UrlPath(path) = regex("\/[a-zA-Z0-9_\/:-]*", path);
Arrow = "->";
}
rule RouteLine {
let verb = expect HttpVerb;
let path = expect UrlPath;
expect Arrow;
let controller = expect Identifier;
expect Symbol(".");
let action = expect Identifier;
return AST.RegisterRoute(verb, path, controller, action);
}
}
routes {
GET /users -> UserController.index
}
fact("route_GET_/users", "registered", "yes")
print(query("route_GET_/users", "registered", 0))
print(query("route_GET_/wrong", "registered", 0))
Output:
1
0
Event + Goal-Oriented
The goal and its bound action are declared once, up front; an event handler is what actually triggers pursuing and activating that goal, whenever the right event arrives later.
# Event + goal-oriented: the goal and its bound action are declared
# once, up front; an event handler is what actually triggers pursuing
# and activating that goal, whenever the right event arrives later.
action_add("chop_wood", [], [["has_wood", "yard"]], [], 1)
action_bind("chop_wood", |args| {
print("chopping wood")
return true
})
goal get_wood {
has_wood(yard)
}
when want_wood {
let p = pursue get_wood
print(activate p)
}
emit("want_wood")
Output:
chopping wood
true
Event + DSL
Routes are declared via the custom syntax; a plain event handler independently reacts whenever a "route_added" notification is raised, entirely unaware of the DSL that produced it.
# Event + DSL: routes are declared via the custom syntax; a plain
# event handler independently reacts whenever a "route_added"
# notification is raised, entirely unaware of the DSL that produced it.
syntax RouterDSL {
trigger: Keyword("routes");
tokens {
HttpVerb(verb) = regex("\b(GET|POST|PUT|DELETE)\b", verb);
UrlPath(path) = regex("\/[a-zA-Z0-9_\/:-]*", path);
Arrow = "->";
}
rule RouteLine {
let verb = expect HttpVerb;
let path = expect UrlPath;
expect Arrow;
let controller = expect Identifier;
expect Symbol(".");
let action = expect Identifier;
return AST.RegisterRoute(verb, path, controller, action);
}
}
when route_added {
print("a route was added: " + event_data)
}
routes {
GET /users -> UserController.index
}
emit("route_added", "/users")
Output:
a route was added: /users
Goal-Oriented + DSL
The custom route syntax declares the routes; a separately-planned goal represents "the router is ready to serve requests," achieved by an ordinary bound action — two independent ways of expressing "what needs to happen," used side by side.
# Goal-oriented + DSL: the custom route syntax declares the routes; a
# separately-planned goal represents "the router is ready to serve
# requests," achieved by an ordinary bound action -- two independent
# ways of expressing "what needs to happen," used side by side.
syntax RouterDSL {
trigger: Keyword("routes");
tokens {
HttpVerb(verb) = regex("\b(GET|POST|PUT|DELETE)\b", verb);
UrlPath(path) = regex("\/[a-zA-Z0-9_\/:-]*", path);
Arrow = "->";
}
rule RouteLine {
let verb = expect HttpVerb;
let path = expect UrlPath;
expect Arrow;
let controller = expect Identifier;
expect Symbol(".");
let action = expect Identifier;
return AST.RegisterRoute(verb, path, controller, action);
}
}
routes {
GET /users -> UserController.index
}
action_add("register_route_handler", [], [["route_ready", "true"]], [], 1)
action_bind("register_route_handler", |args| {
print("route handler ready")
return true
})
goal ready_for_requests {
route_ready(true)
}
let p = pursue ready_for_requests
print(activate p)
Output:
route handler ready
true
Triples
Functional + OO + Logic
Real Person objects, built and read via dot notation (p.dob = 1996 / p.name), get asserted as logic facts and reasoned over exactly like the Functional + Logic pair above — rule chaining, and classify/age_from_dob genuinely passed as values into map_list/filter_list. This is also the example that surfaced a real, previously-dead-code parser bug: obj.prop = value had never actually been reachable, because a bare = is also a tolerated equality operator and the general expression parser always consumed it as a comparison before the dedicated assignment code ever got a chance to see it.
# Functional + OO + Logic (a triple, added directly at the user's request
# while building out the Functional + Logic pair above -- not part of the
# planned triples backlog batch, just a natural extension of it): real
# Person OBJECTS, built and filled in using dot notation for both writing
# (`p.dob = 1996`) and reading (`p.name`), get ASSERTED as logic facts,
# then reasoned over exactly like the plain Functional + Logic example --
# rule chaining, and classify/age_from_dob genuinely passed as values
# into map_list/filter_list.
#
# This is also the example that surfaced a real, previously-dead-code
# parser bug: `obj.prop = value` (Stmt::MemberAssign) had NEVER actually
# been reachable in this parser, because a bare `=` is ALSO a tolerated
# equality operator (`if 1 = 1 then ...`) -- parse_expression's own Pratt
# loop always consumed "= value" as a comparison before the statement-
# level member-assignment check ever got a chance to see a pending `=`.
# Fixed in parser.rs's operator loop: a bare `=` directly after a Member
# expression, outside an if/while condition, now stops before consuming
# it, letting the (real, already-existing) member-assignment code handle
# it -- verified this doesn't disturb genuine `==` comparisons (a
# distinct token, unaffected) or the `if 1 = 1` tolerance (guarded by the
# same condition-context flag already used for that).
make a function called age_from_dob takes birth_year returns years
return 2026 - birth_year
end
make a function called classify takes age returns label
if age < 18 then
return "minor"
end
return "adult"
end
make a function called map_list takes items, f returns out
let out = []
let i = 0
let n = to_num(list_len(items))
while i < n do
let out = list_push(out, f(items[i]))
let i = i + 1
end
return out
end
make a function called filter_list takes items, pred returns out
let out = []
let i = 0
let n = to_num(list_len(items))
while i < n do
if pred(items[i]) then
let out = list_push(out, items[i])
end
let i = i + 1
end
return out
end
rule person_dob(X, D) :- person(X), dob(X, D).
let p_alice = new("Person", "p_alice")
p_alice.name = "alice"
p_alice.dob = 1996
let p_bob = new("Person", "p_bob")
p_bob.name = "bob"
p_bob.dob = 2015
let people = [p_alice, p_bob]
let assert_person = |p| { rule_add("person", [p.name], []); rule_add("dob", [p.name, p.dob], []) }
let unused = map_list(people, assert_person)
let pairs = solve("person_dob", ["X", "D"])
let classify_pair = |pair| { classify(age_from_dob(to_num(pair[1]))) }
print(map_list(pairs, classify_pair))
let name_of = |pair| { pair[0] }
let is_adult = |pair| { classify(age_from_dob(to_num(pair[1]))) == "adult" }
print(map_list(filter_list(pairs, is_adult), name_of))
Output:
[adult, minor]
[alice]
See also
PatLang Paradigms Guide for a fuller treatment of each paradigm on its own. The PatLang Compiler Pipeline for how these all get lowered through the same shared front end regardless of which paradigms a program mixes. Capabilities & Honest Limitations for the top-level-let-in-a-function gotcha the Functional + Event example works around.