PONY λ M2 Modula-2

Ruby.CodeCompared.To/Prolog

An interactive executable cheatsheet for Rubyists learning Prolog

Ruby 4.0 Tau Prolog 0.3
Facts & Queries
Hello, World
puts "Hello, World!"
:- writeln('Hello, World!').
The line beginning with :- is a directive — a goal that Prolog runs immediately when it reads the program. writeln/1 writes its argument followed by a newline. There is no main function in Prolog; the program is the goal.
Facts About the World
people = ["alice", "bob", "carol"] people.each { |person| puts "#{person} is a person" }
person(alice). person(bob). person(carol). :- forall(person(Who), (write(Who), writeln(' is a person'))).
A fact like person(alice). declares something true. forall/2 walks every solution of its first argument and runs the second for each. Notice the lowercase names — in Prolog, lowercase identifiers are atoms (constants), while uppercase are variables.
Atoms vs. Variables
# Ruby has no syntactic distinction — # capitalization is just a style convention. alice = "alice" Bob = "Bob" puts alice puts Bob
% lowercase = atom (a constant value) % Uppercase = variable (matches anything) :- X = alice, write('X is '), writeln(X). :- alice = alice, writeln('alice unifies with alice').
The single most important Prolog rule: capitalization determines meaning. alice is an atom (a literal name). X is a variable — a placeholder that the engine tries to bind to a value. This is the opposite of Ruby, where X is just a (warned-against) constant name.
Queries Find All Answers
likes = [ ["alice", "books"], ["alice", "tea"], ["bob", "coffee"], ] likes.select { |person, _| person == "alice" } .each { |_, thing| puts thing }
likes(alice, books). likes(alice, tea). likes(bob, coffee). :- forall(likes(alice, Thing), writeln(Thing)).
The query likes(alice, Thing) asks: "for what values of Thing is this fact true?" Prolog finds every match automatically — no loop, no filter, no enumerable. The engine's job is to enumerate solutions; yours is to describe the relationship.
Multi-Argument Facts
books = [ { title: "Dune", author: "Herbert", year: 1965 }, { title: "Neuromancer", author: "Gibson", year: 1984 }, ] books.each do |book| puts "#{book[:title]} by #{book[:author]} (#{book[:year]})" end
book(dune, herbert, 1965). book(neuromancer, gibson, 1984). :- forall( book(Title, Author, Year), format("~w by ~w (~w)~n", [Title, Author, Year]) ).
Each fact is a relation: book(Title, Author, Year) is a 3-place relation, written book/3. format/2 is Prolog's printf: ~w writes a term, ~n emits a newline. Facts are arguments-only — there are no named fields.
Unification
Unification with =
# Ruby has no unification — # = is one-directional assignment. greeting = "hello" puts greeting
:- X = 42, write('X = '), writeln(X). :- foo(A, B) = foo(1, 2), format("A=~w, B=~w~n", [A, B]).
= in Prolog is unification, not assignment. It makes both sides structurally identical, binding variables as needed. foo(A, B) = foo(1, 2) matches the two terms, binding A=1 and B=2 in a single step. This is the engine's core operation.
Unification Can Fail
# Closest Ruby equivalent — case/in pattern matching: case [1, 2] in [3, x] then puts "matched: x=#{x}" else puts "no match" end
:- ( [1, 2] = [3, X] -> format("matched: X=~w~n", [X]) ; writeln('no match') ).
Unification fails when terms cannot be made identical: 1 cannot unify with 3. Prolog 4.0's pattern matching (case/in) is a near-parallel — both descend through structure and bind variables, both fail when the structure disagrees.
The Anonymous Variable _
pairs = [[:a, 1], [:b, 2], [:c, 3]] first_elements = pairs.map { |first, _| first } puts first_elements.inspect
pair(a, 1). pair(b, 2). pair(c, 3). % Underscore = "I don't care what this is" :- findall(First, pair(First, _), Firsts), writeln(Firsts).
The underscore _ is the anonymous variable — every occurrence is a fresh, unrelated variable. Use it when you need to acknowledge a position without naming it. This is exactly Ruby's |first, _| convention, but enforced by the engine: _ never binds.
Structural Unification
case { name: "alice", age: 30 } in { name: String => name, age: Integer => age } puts "name=#{name}, age=#{age}" end
% Compound terms unify recursively. :- person(name(alice), age(30)) = person(name(N), age(A)), format("name=~w, age=~w~n", [N, A]).
Unification descends through nested structure. person(name(N), age(A)) matches person(name(alice), age(30)) by recursively unifying each sub-term. This is how Prolog inspects data — there is no separate "destructure" operation.
Variables Bind Once
# A Ruby variable can be reassigned at will. x = 1 x = 2 puts x # => 2
:- X = 1, ( X = 2 -> writeln('reassigned (impossible in Prolog)') ; format("X is still ~w — variables don't reassign~n", [X]) ).
Once a Prolog variable is bound, it stays bound for the rest of that proof. X = 1, X = 2 tries to unify the already-bound X with 2 — and fails. There is no mutation in pure Prolog; "rebinding" requires fresh variables.
Rules & Logic
A Rule Is an Implication
def grandparent?(grandparent, grandchild, parents) parents.any? do |gp, parent| gp == grandparent && parents.include?([parent, grandchild]) end end
parent(tom, bob). parent(bob, ann). parent(bob, pat). grandparent(Grandparent, Grandchild) :- parent(Grandparent, Parent), parent(Parent, Grandchild). :- forall(grandparent(tom, X), writeln(X)).
Read :- as "if". The rule says: Grandparent is a grandparent of Grandchild if there exists a Parent in between. The comma is logical "AND". Prolog tries every combination of values that makes the body true.
Conjunction with Comma
def can_drive?(person, age, license) age >= 16 && license end puts can_drive?(:alice, 18, true)
age(alice, 18). age(bob, 12). has_license(alice). can_drive(Person) :- age(Person, Years), Years >= 16, has_license(Person). :- ( can_drive(alice) -> writeln('alice can drive') ; writeln('alice cannot') ). :- ( can_drive(bob) -> writeln('bob can drive') ; writeln('bob cannot') ).
The comma , means "and". Each subgoal must succeed for the rule to fire. Goals are tried left-to-right; if one fails, Prolog backtracks — it undoes earlier bindings and tries different choices to make the conjunction work.
Disjunction with Semicolon
def free?(day) day == "saturday" || day == "sunday" end puts free?("saturday") puts free?("monday")
free(Day) :- ( Day = saturday ; Day = sunday ). :- ( free(saturday) -> writeln('saturday: free') ; writeln('saturday: not free') ). :- ( free(monday) -> writeln('monday: free') ; writeln('monday: not free') ).
The semicolon ; means "or". It can appear inside a rule body. More commonly, "or" is expressed by writing multiple clauses — each clause is an alternative way for the head to be true. Both forms behave the same; multi-clause style is more idiomatic.
Multiple Clauses = Or
def weekend?(day) day == :saturday || day == :sunday end puts weekend?(:saturday)
weekend(saturday). weekend(sunday). :- ( weekend(saturday) -> writeln('saturday is weekend') ; true ), ( weekend(monday) -> writeln('monday is weekend') ; writeln('monday is not weekend') ).
Two facts with the same head form a disjunction: anything that matches either clause succeeds. This is the most common style — you read it top-to-bottom as a series of alternatives the engine will try in order.
Negation as Failure
vegetarians = [:alice, :bob] people = [:alice, :bob, :carol] non_vegetarians = people.reject { |p| vegetarians.include?(p) } puts non_vegetarians.inspect
person(alice). person(bob). person(carol). vegetarian(alice). vegetarian(bob). % \+ means "cannot prove" — closed-world negation :- forall((person(P), \+ vegetarian(P)), writeln(P)).
\+ Goal succeeds when Prolog cannot prove Goal — known as "negation as failure". This is not logical negation: it assumes the database is closed (anything not provable is false). The escape \\+ in the JS template becomes a literal \+ in the program.
Arithmetic
is/2 Evaluates; = Unifies
# In Ruby, = always assigns the right side after evaluation. sum = 2 + 3 puts sum # => 5
:- X = 2 + 3, format("X = ~w (just the term, unevaluated)~n", [X]). :- Y is 2 + 3, format("Y = ~w (arithmetic result)~n", [Y]).
Two distinct operators. X = 2 + 3 unifies X with the compound term 2+3 — it does not compute. Y is 2 + 3 forces arithmetic evaluation and binds Y to 5. Forgetting this distinction is the most common Prolog beginner bug.
Basic Operations
puts 7 + 3 puts 7 - 3 puts 7 * 3 puts 7.0 / 3 puts 7 / 3 # integer division in Ruby for Integer/Integer puts 7 % 3
:- A is 7 + 3, writeln(A). :- B is 7 - 3, writeln(B). :- C is 7 * 3, writeln(C). :- D is 7 / 3, writeln(D). % float division :- E is 7 // 3, writeln(E). % integer division :- F is 7 mod 3, writeln(F).
Prolog distinguishes float division / from integer division //, similar to Python. mod is the modulo operator. The right-hand side of is must contain only numbers and arithmetic operators — passing an unbound variable raises an error.
Math Functions
puts (-5).abs puts [3, 7, 2].min puts [3, 7, 2].max puts Math.sqrt(16)
:- A is abs(-5), writeln(A). :- B is min(3, 7), writeln(B). :- C is max(3, 7), writeln(C). :- D is sqrt(16), writeln(D).
Prolog supports the usual math functions on the right of is: abs, min, max, sqrt, sin, cos, log, exp, and more. These are functions (not predicates) — they appear only inside arithmetic evaluation.
Range with between/3
(1..5).each { |number| puts number }
:- forall(between(1, 5, N), writeln(N)).
between(Low, High, X) succeeds when X is an integer in that range — and generates each one in turn on backtracking. It plays the same role as Ruby's 1..5, but as a relation: you can also test whether a known value falls in range.
Successor — succ/2
puts 5 + 1 # 6 puts 5 - 1 # 4
:- succ(5, Next), format("after 5: ~w~n", [Next]). :- succ(Prev, 5), format("before 5: ~w~n", [Prev]).
succ(X, Y) means "Y is the successor of X" — that is, Y = X + 1, with both X and Y non-negative integers. Like most Prolog predicates it is relational: given either argument, it computes the other. This bidirectionality is the heart of Prolog.
Comparison
Arithmetic Comparison
puts 5 < 10 puts 5 > 10 puts 5 == 5 puts 5 != 10
:- ( 5 < 10 -> writeln('5 < 10') ; writeln('not <') ). :- ( 5 > 10 -> writeln('5 > 10') ; writeln('not >') ). :- ( 5 =:= 5 -> writeln('5 =:= 5') ; writeln('not =:=') ). :- ( 5 =\= 10 -> writeln('5 =\\= 10') ; writeln('not =\\=') ).
For numbers use <, >, =<, >=, =:= (numerically equal), and =\= (numerically different). Both sides must be fully evaluable expressions. Unlike =, these never bind variables — they only compare.
Numeric vs. Term Equality
# Ruby uses == for both; # the type system disambiguates. puts 1 + 2 == 3 # true puts "1+2" == "3" # false (string vs. string)
:- ( 1 + 2 =:= 3 -> writeln('=:= is true (numeric)') ; writeln('false') ). :- ( 1 + 2 == 3 -> writeln('== is true (term-identical)') ; writeln('== is false') ).
=:= evaluates both sides as arithmetic. == asks whether the two terms are structurally identical — and 1+2 is the compound +(1,2), which is not identical to the integer 3. Choose the comparison based on what kind of equality you mean.
\= — Cannot Unify
# Closest Ruby — pattern non-match: case [1, 2] in [1, 2] then puts "matched" else puts "did not match" end
:- ( foo(1) \= foo(2) -> writeln('foo(1) and foo(2) cannot unify') ; writeln('they unify') ). :- ( foo(X) \= foo(1) -> writeln('cannot unify') ; writeln('foo(X) unifies with foo(1)') ).
X \= Y succeeds when X and Y cannot be unified. Note the second case: foo(X) \= foo(1) fails, because the variable X can unify with 1. This is different from saying X is not equal to 1 — it asks whether they can be made identical.
Standard Order of Terms
puts ([3, 1, 2].sort).inspect
:- msort([banana, 3, apple, 1, [1,2]], Sorted), writeln(Sorted).
Prolog defines a total order on all terms: numbers < atoms < strings < compound terms. The @<, @>, @=<, @>= operators compare terms in this order. msort/2 sorts a list using this order, regardless of element type.
compare/3 — Three-Way Comparison
result = 3 <=> 5 puts result # => -1
:- compare(Order, 3, 5), format("3 vs 5: ~w~n", [Order]). :- compare(Order, hello, hello), format("hello vs hello: ~w~n", [Order]).
compare(Order, X, Y) binds Order to <, =, or > depending on the standard order of terms. It corresponds to Ruby's spaceship <=>, but works on any pair of terms — not just comparables.
Lists
List Literals
numbers = [1, 2, 3, 4] puts numbers.inspect puts numbers.length
:- Numbers = [1, 2, 3, 4], writeln(Numbers), length(Numbers, Len), format("length: ~w~n", [Len]).
Prolog lists use the same [...] syntax as Ruby. Internally they are linked structures built from a "cons" operator and the empty list []. length/2 relates a list to its length — given the list, it computes the length; given only a length, it generates a list of fresh variables.
Head/Tail Pattern
numbers = [10, 20, 30, 40] head, *tail = numbers puts "head: #{head}" puts "tail: #{tail.inspect}"
:- Numbers = [10, 20, 30, 40], Numbers = [Head | Tail], format("head: ~w~n", [Head]), format("tail: ~w~n", [Tail]).
The pipe | separates the first element from the rest: [Head | Tail] matches any non-empty list. This is destructuring by unification — the same operation that drives every Prolog match. You can split off more: [A, B | Rest] peels two elements.
member/2 — Membership
letters = [:a, :b, :c, :d] puts letters.include?(:c) letters.each { |letter| puts letter }
:- ( member(c, [a, b, c, d]) -> writeln('c is a member') ; writeln('c is not') ). :- forall(member(L, [a, b, c, d]), writeln(L)).
member(X, List) succeeds when X is an element of List. Like most Prolog predicates it works two ways: with a bound X, it tests membership; with an unbound X, it generates each member in turn through backtracking.
append/3 — Bidirectional
puts ([1, 2] + [3, 4]).inspect # splitting a list requires explicit code: list = [1, 2, 3, 4] (0..list.length).each do |index| puts "#{list.take(index).inspect} ++ #{list.drop(index).inspect}" end
:- append([1, 2], [3, 4], Joined), writeln(Joined). % Run backwards — find all ways to split [1,2,3,4]: :- forall( append(Left, Right, [1, 2, 3, 4]), format("~w ++ ~w~n", [Left, Right]) ).
append(A, B, C) means "C is the concatenation of A and B". A single rule answers three questions: concatenate (A, B known), split (C known), or generate all pairs that concatenate to a given list. Ruby needs three separate algorithms for the same task.
reverse/2
puts [1, 2, 3, 4].reverse.inspect
:- reverse([1, 2, 3, 4], Reversed), writeln(Reversed).
reverse(List, Reversed) reverses a list. It is a built-in but is also a classic first recursive exercise — the textbook implementation walks the list and accumulates into a second list, which produces the reverse for free.
Indexing — nth0/3 and nth1/3
letters = [:a, :b, :c, :d] puts letters[0] # :a (0-based) puts letters[2] # :c
:- nth0(0, [a, b, c, d], First), format("nth0(0): ~w~n", [First]). :- nth0(2, [a, b, c, d], Third), format("nth0(2): ~w~n", [Third]). :- nth1(1, [a, b, c, d], FirstByOne), format("nth1(1): ~w~n", [FirstByOne]).
Prolog has both nth0/3 (zero-indexed, like Ruby) and nth1/3 (one-indexed, like the natural numbering "first, second, third"). Picking your conventions explicitly is more pleasant than guessing.
Recursion
List Length (Manual Recursion)
def my_length(list) return 0 if list.empty? 1 + my_length(list[1..]) end puts my_length([:a, :b, :c, :d])
% Base case: empty list has length 0 my_length([], 0). % Recursive case: 1 + length of tail my_length([_ | Tail], Length) :- my_length(Tail, TailLength), Length is TailLength + 1. :- my_length([a, b, c, d], N), format("length: ~w~n", [N]).
The Prolog pattern: a base case clause for empty/zero, plus a recursive case that peels one element and relates the smaller problem to the answer. There is no loop construct — every iteration in Prolog is a recursive call.
Sum of a List
def sum(list) return 0 if list.empty? list.first + sum(list[1..]) end puts sum([10, 20, 30, 40]) # Or idiomatic: puts [10, 20, 30, 40].sum
sum_list([], 0). sum_list([Head | Tail], Total) :- sum_list(Tail, RestTotal), Total is Head + RestTotal. :- sum_list([10, 20, 30, 40], Total), format("sum: ~w~n", [Total]).
The same shape as length: empty list sums to 0; non-empty list sums to head plus the sum of the tail. The standard library actually provides sum_list/2 already; writing it yourself is the canonical "first useful recursion" exercise.
Factorial
def factorial(number) return 1 if number <= 1 number * factorial(number - 1) end puts factorial(6)
factorial(0, 1). factorial(N, Result) :- N > 0, Previous is N - 1, factorial(Previous, PreviousResult), Result is N * PreviousResult. :- factorial(6, F), format("6! = ~w~n", [F]).
The two clauses cover the two cases by pattern: factorial(0, 1) handles the base, and the rule handles positive N. The guard N > 0 prevents the rule from being tried for the base case, where its body would loop infinitely.
Fibonacci
def fib(number) return number if number < 2 fib(number - 1) + fib(number - 2) end puts fib(10)
fib(0, 0). fib(1, 1). fib(N, F) :- N > 1, N1 is N - 1, N2 is N - 2, fib(N1, F1), fib(N2, F2), F is F1 + F2. :- fib(10, F), format("fib(10) = ~w~n", [F]).
Three clauses, three cases. This is the naive recursive Fibonacci — exponential in time, just like the Ruby version. Real Prolog programs would use an accumulator or memoization (via assertz/1) for efficiency.
Print Each Element
[:a, :b, :c].each { |item| puts item }
print_each([]). print_each([Head | Tail]) :- writeln(Head), print_each(Tail). :- print_each([a, b, c]).
A side-effecting recursion: walk the list, print, recurse. The base case does nothing (the empty list has nothing to print). In idiomatic Prolog you would write forall(member(X, List), writeln(X)) — but writing the recursion by hand exposes the underlying shape.
Atoms & Strings
Atoms vs. Strings
# Closest Ruby parallel — # Symbols (atoms) vs. Strings: puts :alice.class # Symbol puts "alice".class # String
:- writeln(alice). % atom :- writeln('hello world'). % quoted atom (spaces or special chars) :- writeln("hello world"). % string (in modern Prolog)
An atom is a symbolic constant — like a Ruby symbol but without the leading colon. Lowercase identifiers are atoms; any text inside single quotes is also an atom. Double-quoted text is a string in Tau Prolog and most modern Prologs.
Joining Atoms
first = "hello" second = "world" puts "#{first} #{second}" puts (first + " " + second)
:- atom_concat(hello, ' world', Joined), writeln(Joined). % Three-way: split a known atom :- atom_concat(Left, Right, helloworld), format("~w + ~w~n", [Left, Right]), fail ; true.
atom_concat/3 joins two atoms — and like append/3, it runs backwards: given the result, it finds every way to split it into two pieces. The trailing fail ; true drives the loop through every split, then succeeds at the end.
Length of an Atom
puts "prolog".length
:- atom_length(prolog, Length), format("length: ~w~n", [Length]).
atom_length(Atom, Length) relates an atom to its character count. As always: bound atom, computes length; bound length, generates fresh atoms of that length (less commonly useful).
Atom ↔ Characters
puts "abc".chars.inspect puts ["a", "b", "c"].join
:- atom_chars(abc, Chars), writeln(Chars). :- atom_chars(Word, [h, e, l, l, o]), writeln(Word).
atom_chars(Atom, Chars) converts between an atom and a list of one-character atoms. The relational design means the same predicate splits and joins. atom_codes/2 is the analogous predicate that uses integer character codes instead.
Number ↔ Atom Conversion
puts 42.to_s puts "42".to_i
% number_chars/2 relates a number to its character list :- number_chars(42, Chars), atom_chars(AsAtom, Chars), format("atom: ~w~n", [AsAtom]). :- atom_chars('42', Chars), number_chars(AsNumber, Chars), format("number: ~w~n", [AsNumber]).
number_chars(Number, Chars) relates a number to a list of one-character atoms. Combined with atom_chars/2, it converts between atoms and numbers either way. The standard does not provide a single direct predicate — going through characters is the portable approach.
Backtracking & Control
Backtracking Finds All Solutions
colors = [:red, :green, :blue] sizes = [:small, :large] colors.each do |color| sizes.each { |size| puts "#{color}-#{size}" } end
color(red). color(green). color(blue). size(small). size(large). :- forall( (color(Color), size(Size)), format("~w-~w~n", [Color, Size]) ).
Conjunction backtracks like nested loops. Each color(Color) picks a value; for that choice, size(Size) picks a value; the body fires. When the body completes, the engine asks for the next size, then the next color, automatically. This is the Prolog version of nested iteration.
The Cut !
def category(age) return :infant if age < 2 return :child if age < 13 return :adult end puts category(1) puts category(7) puts category(30)
category(Age, infant) :- Age < 2, !. category(Age, child) :- Age < 13, !. category(_, adult). :- category(1, A), writeln(A). :- category(7, B), writeln(B). :- category(30, C), writeln(C).
The cut ! commits to the current clause — it tells the engine "don't try other clauses for this predicate". This turns a multi-clause definition into an if/elsif chain. Used carefully, the cut makes definitions efficient and deterministic; overused, it sabotages the relational nature of the language.
Fail-Driven Loop
[1, 2, 3].each { |number| puts number * 10 }
:- member(N, [1, 2, 3]), Tenfold is N * 10, writeln(Tenfold), fail ; true.
The classical Prolog idiom for "do this for each solution". fail forces backtracking; the engine retries member, picks the next value, runs the body again. When member exhausts its choices, control passes to ; true so the directive succeeds. Modern code prefers forall/2.
If-Then-Else
number = 7 result = number.even? ? "even" : "odd" puts result
:- N = 7, ( 0 is N mod 2 -> Result = even ; Result = odd ), format("~w is ~w~n", [N, Result]).
(Cond -> Then ; Else) is Prolog's if-then-else. If Cond succeeds, only Then runs and the choice is committed; otherwise Else runs. Without the -> arrow you have a plain disjunction, which behaves very differently — it leaves both branches as backtracking choice points.
once/1 — First Solution Only
numbers = [1, 2, 3, 4] first_even = numbers.find(&:even?) puts first_even
:- once((member(N, [1, 2, 3, 4]), 0 is N mod 2)), format("first even: ~w~n", [N]).
once(Goal) succeeds with the first solution of Goal and then commits — no backtracking into Goal. It is equivalent to (Goal, !) wrapped to scope the cut. Use it whenever you want "the first one that works" without retrying.
I/O
write/1 and writeln/1
print "no newline " puts "with newline"
:- write('no newline '), writeln('with newline').
write/1 writes a term without a trailing newline. writeln/1 is the same plus nl. The argument can be any term: atoms, numbers, lists, compound structures — Prolog writes a textual representation of each.
nl/0 — Newline
print "line 1" puts print "line 2" puts
:- write('line 1'), nl, write('line 2'), nl.
nl/0 writes a single newline. It is the lower-level partner to writeln/1. Spell out write + nl when you want to compose output from multiple terms before ending the line.
format/2 — Formatted Output
name = "alice" age = 30 puts "Name: %s, Age: %d" % [name, age] puts "Name: #{name}, Age: #{age}"
:- Name = alice, Age = 30, format("Name: ~w, Age: ~w~n", [Name, Age]). :- format("Name: ~a, Age: ~d~n", [alice, 30]).
format/2 is Prolog's printf. Placeholders: ~w writes any term, ~a writes atoms, ~d writes integers, ~n emits a newline. The second argument is a list of values matched to placeholders in order.
writeq/1 — Quoted Terms
puts "hello world" # bare puts "hello world".inspect # "hello world"
:- write('hello world'), nl. % no quotes :- writeq('hello world'), nl. % quoted, re-readable :- write([1, hello, 'with space']), nl. :- writeq([1, hello, 'with space']), nl.
writeq/1 writes a term in a form that can be read back as input — atoms with special characters get quoted, operators get parentheses if needed. write/1 is for humans; writeq/1 is for round-tripping.
Higher-Order
maplist/2 — Test Every Element
numbers = [2, 4, 6, 8] puts numbers.all?(&:even?)
is_even(N) :- 0 is N mod 2. :- ( maplist(is_even, [2, 4, 6, 8]) -> writeln('all even') ; writeln('not all even') ).
maplist(Predicate, List) succeeds when Predicate is true of every element. The predicate is named by an atom and looked up like any other rule. This is Ruby's .all?, but the "block" is a separately-defined named relation.
maplist/3 — Transform a List
numbers = [1, 2, 3, 4] puts numbers.map { |number| number * 2 }.inspect
double(X, Y) :- Y is X * 2. :- maplist(double, [1, 2, 3, 4], Doubled), writeln(Doubled).
maplist(P, In, Out) applies a 2-place predicate to each input and collects each result. double/2 relates an input to its double. This is Ruby's .map, with the relation explicitly named. The /2 means "two arguments".
maplist/4 — Zip and Combine
puts [1, 2, 3].zip([10, 20, 30]) .map { |first, second| first + second } .inspect
add(X, Y, Sum) :- Sum is X + Y. :- maplist(add, [1, 2, 3], [10, 20, 30], Sums), writeln(Sums).
maplist/4 takes a 3-place predicate plus three lists of equal length, applying the predicate element-wise. It generalises naturally to maplist/5, maplist/6, etc. — Prolog's metaprogramming makes that uniform.
call/N — Meta-Call
predicate = ->(number) { number > 5 } puts predicate.call(10) puts predicate.call(3)
bigger_than(Threshold, Value) :- Value > Threshold. :- ( call(bigger_than(5), 10) -> writeln('10 > 5') ; writeln('not') ), ( call(bigger_than(5), 3) -> writeln('3 > 5') ; writeln('3 is not > 5') ).
call(Goal) invokes a goal stored as data. call(P, Arg) adds an extra argument before calling. This is how higher-order predicates pass partially-applied goals — bigger_than(5) is a "callable" with one slot still to fill, just like a lambda.
forall/2 — Universal Check
numbers = [2, 4, 6] puts numbers.all? { |number| number.even? }
:- ( forall(member(N, [2, 4, 6]), 0 is N mod 2) -> writeln('all are even') ; writeln('found an odd') ).
forall(Cond, Action) succeeds when, for every solution of Cond, Action also succeeds. It is the relational version of "for all X in C, P(X)". A common idiom: pair it with member/2 to express "every element satisfies...".
Collecting Solutions
findall/3
parents = { tom: [:bob, :liz], bob: [:ann, :pat], } children_of_tom = parents[:tom] || [] puts children_of_tom.inspect
parent(tom, bob). parent(tom, liz). parent(bob, ann). parent(bob, pat). :- findall(Child, parent(tom, Child), Children), writeln(Children).
findall(Template, Goal, List) collects every solution of Goal, instantiating Template for each, into a list. It always succeeds, even if there are no solutions — in which case List is [].
bagof/3 — Fails on Empty
items = [1, 2, 3] result = items.select { |item| item > 10 } puts result.empty? ? "none" : result.inspect
item(1). item(2). item(3). :- ( bagof(X, (item(X), X > 10), Big) -> format("big: ~w~n", [Big]) ; writeln('none') ). :- bagof(X, (item(X), X > 1), Some), format("some: ~w~n", [Some]).
bagof/3 is like findall/3, but it fails when there are no solutions instead of binding to []. Use it when "no answers" is meaningful and should propagate as failure, not as an empty list.
setof/3 — Sorted, Unique
colors = [:red, :blue, :red, :green, :blue] puts colors.uniq.sort.inspect
favorite(alice, red). favorite(alice, blue). favorite(bob, red). favorite(carol, green). favorite(dave, blue). :- setof(Color, Who^favorite(Who, Color), Colors), writeln(Colors).
setof/3 collects solutions, sorts them, and removes duplicates. The Who^ says "existentially quantified" — we don't care who picked each color, we just want the colors. Without that marker, setof would group results per distinct Who.
Aggregate via findall
numbers = [1, 2, 3, 4, 5] puts numbers.sum
number(1). number(2). number(3). number(4). number(5). :- findall(N, number(N), All), sum_list(All, Total), format("sum: ~w~n", [Total]).
A classic pattern: findall/3 harvests every solution into a list, then list-processing predicates aggregate. sum_list/2, max_list/2, min_list/2, length/2 all work this way.
Dynamic Database
assertz/1 — Add a Fact at Runtime
seen = [] seen << :alice seen << :bob puts seen.inspect
:- dynamic(seen/1). :- assertz(seen(alice)), assertz(seen(bob)), findall(X, seen(X), All), writeln(All).
assertz/1 adds a new fact to the database at the end; asserta/1 adds at the start. The :- dynamic(name/arity). declaration is required: it tells Prolog this predicate can be modified at runtime, which a pure logic program normally cannot do.
retract/1 — Remove a Fact
items = [:apple, :banana, :cherry] items.delete(:banana) puts items.inspect
:- dynamic(item/1). item(apple). item(banana). item(cherry). :- retract(item(banana)), findall(X, item(X), Remaining), writeln(Remaining).
retract/1 removes the first matching clause. Asserts and retracts together let Prolog programs maintain mutable state — but at the cost of pure logical reasoning. Treat them as side-effects, like Ruby's instance variables.
A Mutable Counter
class Counter def initialize; @value = 0; end def increment; @value += 1; end def value; @value; end end counter = Counter.new 3.times { counter.increment } puts counter.value
:- dynamic(counter/1). counter(0). increment :- retract(counter(Old)), New is Old + 1, assertz(counter(New)). :- increment, increment, increment, counter(Value), format("counter: ~w~n", [Value]).
The cycle retract → arithmetic → assertz updates a single fact in place. It is the Prolog way to simulate an assignable variable. In real Prolog code, prefer threading state through arguments — but for genuinely global state, this is the idiom.
Adding a Rule at Runtime
# Closest Ruby — define_method: class Greeter; end Greeter.define_method(:hi) { "hello!" } puts Greeter.new.hi
:- dynamic(greeting/1). :- assertz((greeting(Hour) :- Hour < 12, writeln('Good morning!'))), assertz((greeting(Hour) :- Hour >= 12, writeln('Good afternoon!'))), greeting(9), greeting(15).
You can assert rules, not just facts, by wrapping the clause in parentheses. The added clauses are immediately usable. This is Prolog's metaprogramming: code that writes code, then runs it. Ruby's define_method is the closest mainstream analogue.
Classic Examples
Family Relationships
parents = { tom: [:bob, :liz], bob: [:ann, :pat], pat: [:jim], } def ancestor?(parents, ancestor, descendant) return true if (parents[ancestor] || []).include?(descendant) (parents[ancestor] || []).any? do |child| ancestor?(parents, child, descendant) end end puts ancestor?(parents, :tom, :jim)
parent(tom, bob). parent(tom, liz). parent(bob, ann). parent(bob, pat). parent(pat, jim). ancestor(Older, Younger) :- parent(Older, Younger). ancestor(Older, Younger) :- parent(Older, Middle), ancestor(Middle, Younger). :- ( ancestor(tom, jim) -> writeln('tom is an ancestor of jim') ; writeln('not an ancestor') ), findall(D, ancestor(tom, D), Descendants), format("descendants of tom: ~w~n", [Descendants]).
The canonical Prolog example. Two clauses for ancestor cover the two cases: a direct parent, or a parent of an ancestor. The same definition answers "is X an ancestor of Y?", "who are X's descendants?", and "who are Y's ancestors?" — all from one rule, free.
Path in a Graph
edges = { a: [:b, :c], b: [:d], c: [:d], d: [:e], } def path(edges, source, target, visited = []) return true if source == target (edges[source] || []).any? do |next_node| !visited.include?(next_node) && path(edges, next_node, target, visited + [source]) end end puts path(edges, :a, :e)
edge(a, b). edge(a, c). edge(b, d). edge(c, d). edge(d, e). path(Start, End, [Start, End]) :- edge(Start, End). path(Start, End, [Start | Rest]) :- edge(Start, Middle), path(Middle, End, Rest). :- path(a, e, Route), format("a path from a to e: ~w~n", [Route]). :- findall(P, path(a, e, P), Paths), format("all paths: ~w~n", [Paths]).
Graph traversal as logical declaration: a path either is an edge, or is an edge followed by a path. The recursive clause builds the route by accumulating in a list. Note the cycle hazard: this naive version assumes the graph is acyclic. A real implementation tracks visited nodes.
Minimum of a List
puts [3, 1, 4, 1, 5, 9, 2, 6].min
list_min([Single], Single). list_min([Head | Tail], Min) :- list_min(Tail, TailMin), ( Head < TailMin -> Min = Head ; Min = TailMin ). :- list_min([3, 1, 4, 1, 5, 9, 2, 6], Min), format("min: ~w~n", [Min]).
Two clauses: a single-element list is its own minimum; a longer list's minimum is whichever is smaller, the head or the minimum of the tail. The if-then-else picks one. The base case could use [X] instead of [Single] for the same effect.
Is a List Sorted?
def sorted?(list) list.each_cons(2).all? { |first, second| first <= second } end puts sorted?([1, 2, 2, 3, 5]) puts sorted?([1, 3, 2])
sorted([]). sorted([_]). sorted([First, Second | Rest]) :- First =< Second, sorted([Second | Rest]). :- ( sorted([1, 2, 2, 3, 5]) -> writeln('[1,2,2,3,5] sorted') ; writeln('not') ), ( sorted([1, 3, 2]) -> writeln('[1,3,2] sorted') ; writeln('[1,3,2] not sorted') ).
Three clauses: empty list and single-element list are trivially sorted; a longer list is sorted if the first two elements are in order and the tail (without the head) is also sorted. Sliding a two-element window is natural in head-tail patterns.
Reverse with an Accumulator
def my_reverse(list, accumulator = []) return accumulator if list.empty? my_reverse(list[1..], [list.first] + accumulator) end puts my_reverse([1, 2, 3, 4]).inspect
my_reverse(List, Reversed) :- my_reverse(List, [], Reversed). my_reverse([], Accumulator, Accumulator). my_reverse([Head | Tail], Accumulator, Reversed) :- my_reverse(Tail, [Head | Accumulator], Reversed). :- my_reverse([1, 2, 3, 4], R), format("reversed: ~w~n", [R]).
The accumulator pattern. Each recursive call moves one element from the input list onto the front of the accumulator — when the input is empty, the accumulator is the answer. This is linear time, unlike the naive append-based reverse, which is quadratic.