Output & Running
Hello, World
io:format("Hello, World!~n"). :- writeln('Hello, World!'). Erlang prints with
io:format/1 and the ~n directive for the newline. Prolog runs a program by executing its :- Goal. directives as soon as they are read; writeln/1 writes a term followed by a newline. Neither language has a main function — the directives (Prolog) or the top-level expressions (Erlang shell) simply run in order.Comments use the same character
% This is an Erlang comment, running to end of line.
io:format("~p~n", [ok]). % This is a Prolog comment, running to end of line.
:- writeln(ok). A small, pleasant coincidence: both languages use
% for a line comment. Prolog also supports C-style /* block comments */, which Erlang does not.Printing a term for inspection
Value = {ok, [1, 2, 3]},
io:format("~p~n", [Value]). :- Value = point(1, [2, 3]),
write(Value), nl. Erlang's
~p ("pretty print") directive renders any term for humans, including nested tuples and lists. Prolog's write/1 plays the same role: it prints a compound term (here point(1, [2,3])) in standard Prolog notation, functor followed by parenthesized arguments — the direct analogue of an Erlang tagged tuple.Facts & Queries
Facts about the world
People = [alice, bob, carol],
lists:foreach(fun(Person) -> io:format("~p is a person~n", [Person]) end, People). person(alice).
person(bob).
person(carol).
:- forall(person(Who), (write(Who), writeln(' is a person'))). Erlang has no built-in fact database, so a Rubyist-turned-Erlang-programmer models "alice is a person" as list membership and iterates with
lists:foreach/2. Prolog gives facts a first-class syntax: person(alice). declares something true directly, and forall/2 walks every solution of its first argument, running the second goal for each.Atoms already feel like home
Status = ok,
io:format("~p is an atom: ~p~n", [Status, is_atom(Status)]). :- Status = ok,
( atom(Status) -> writeln('ok is an atom') ; writeln('not an atom') ). This is the single biggest head start an Erlang developer has over, say, a Rubyist or Elixir developer learning Prolog: lowercase identifiers are atoms in both languages, with no punctuation required (unlike Elixir's
:ok). is_atom/1 in Erlang and atom/1 in Prolog even share the same name pattern.Finding every match
Likes = [{alice, books}, {alice, tea}, {bob, coffee}],
Matches = [Thing || {Person, Thing} <- Likes, Person =:= alice],
lists:foreach(fun(Thing) -> io:format("~p~n", [Thing]) end, Matches). likes(alice, books).
likes(alice, tea).
likes(bob, coffee).
:- forall(likes(alice, Thing), writeln(Thing)). The Erlang side needs an explicit list comprehension with a guard to filter down to Alice's rows — you are telling the machine how to search. The query
likes(alice, Thing) asks Prolog "for what values of Thing is this true?" and the engine finds every match on its own — you describe what, not how.Capitalization determines meaning — in Prolog
% Erlang: capitalization ALSO matters, but for a different reason —
% Variable names must start uppercase; atoms must start lowercase.
X = alice,
io:format("~p~n", [X]). % Prolog: the SAME rule, same reason — Uppercase = variable, lowercase = atom.
:- X = alice, write('X is '), writeln(X). Unlike the jump from Ruby or Python, an Erlang developer already has this rule memorized: a name starting with an uppercase letter (or underscore) is a variable; a name starting lowercase is an atom. Prolog enforces the identical convention for the identical reason — this is one of the few languages where the habit transfers with zero relearning.
Unification & Matching
`=` is unification in both languages
{X, Y} = {1, 2},
io:format("~p and ~p~n", [X, Y]). :- point(X, Y) = point(1, 2),
format("~w and ~w~n", [X, Y]). This is the deepest structural kinship between the two languages: in Erlang,
= is not assignment, it is unification — exactly what Prolog's = does. Both destructure a compound term and bind any unbound variables in one step, in a single pass over the shape. (Prolog has no built-in tuple syntax like Erlang's {X, Y} — an ordinary compound term such as point(X, Y) plays that role instead.)A mismatched shape fails to unify
% Erlang: {ok, Value} = {error, not_found} would raise {badmatch, ...}
{ok, Value} = {ok, 42},
io:format("~p~n", [Value]). % Prolog: point(X, Y) = line(1, 2) would simply fail (no exception)
:- point(X, Y) = point(1, 2),
format("~w and ~w~n", [X, Y]). When the shapes genuinely disagree, the languages diverge on how loud the failure is. In Erlang a failed match is a crash — it raises
{badmatch, Value}, an exception that (absent a try) brings down the process. In Prolog a failed unification is just a normal, quiet false — the goal fails, and if the engine has other choices to try (see backtracking, below), it moves on to them instead of crashing anything.The anonymous variable, `_`
{_, Second, _} = {first, "keep me", third},
io:format("~p~n", [Second]). :- point(_, Second, _) = point(first, 'keep me', third),
writeln(Second). Another exact match: the underscore
_ means "match anything here, and do not bother binding a name" in both languages. Neither language warns about an unused _ the way it would warn about an unused named variable.Unifying nested structures
{ok, [First | Rest]} = {ok, [1, 2, 3]},
io:format("~p then ~p~n", [First, Rest]). :- Result = ok([1, 2, 3]),
Result = ok([First | Rest]),
format("~w then ~w~n", [First, Rest]). Unification recurses through nested structure in both languages — a tagged tuple wrapping a cons list unifies element by element, binding
First and Rest in the same motion. The [Head | Tail] cons-cell syntax you already use daily in Erlang is, character for character, the same syntax Prolog uses.Rules vs. Function Clauses
A rule is like a function clause with a name
IsAdult = fun(Age) -> Age >= 18 end,
io:format("~p~n", [IsAdult(20)]). is_adult(Age) :- Age >= 18.
:- ( is_adult(20) -> writeln(true) ; writeln(false) ). A Prolog rule —
head :- body. — reads "head is true if body is true," which is the same relationship an Erlang fun or top-level function expresses between its arguments and its guarded body. The difference is that a rule is a logical statement you can query in either direction, not merely a computation you call.Conjunction: comma means "and"
CanVote = fun(Age, Registered) -> (Age >= 18) andalso Registered end,
io:format("~p~n", [CanVote(20, true)]). can_vote(Age, Registered) :-
Age >= 18,
Registered == true.
:- ( can_vote(20, true) -> writeln(true) ; writeln(false) ). The comma means "and" in the body of a Prolog rule, exactly the way it separates the shell expressions you already write in Erlang — both read as a sequence that must all succeed. The difference is that in Prolog each comma-separated goal can itself backtrack into multiple solutions, not just evaluate once.
Disjunction: two ways to succeed
IsWeekend = fun(Day) -> (Day =:= sat) orelse (Day =:= sun) end,
io:format("~p~n", [IsWeekend(sun)]). is_weekend(sat).
is_weekend(sun).
:- ( is_weekend(sun) -> writeln(true) ; writeln(false) ). Erlang's
orelse is a boolean short-circuit. Prolog usually expresses "or" not with the ; operator but with multiple clauses for the same predicate — is_weekend(sat). and is_weekend(sun). are two independent facts, and a query succeeds if either one matches. This is the more idiomatic Prolog shape, and it is also how it handles what Erlang would call overloaded function clauses.Multiple clauses, chosen by matching
Describe = fun
Describe(0) -> "zero";
Describe(N) when N > 0 -> "positive";
Describe(_) -> "negative"
end,
io:format("~s~n", [Describe(-5)]). describe(0, "zero").
describe(N, "positive") :- N > 0.
describe(_, "negative").
:- describe(-5, Result), writeln(Result). Both languages pick a clause by matching the call against each clause head in order, top to bottom, falling through when a guard fails. The Erlang
when N > 0 guard and the Prolog N > 0 body goal play the same role: both reject a structurally-matching clause when the extra condition is false, letting the next clause have a turn.Arithmetic
Arithmetic evaluation needs `is` in Prolog
% Erlang evaluates arithmetic directly wherever an expression is expected:
Total = 2 + 3 * 4,
io:format("~p~n", [Total]). % Prolog: = is unification, NOT evaluation — you must ask for evaluation with is/2.
:- Total is 2 + 3 * 4,
writeln(Total). This is the sharpest arithmetic gotcha for anyone arriving from Erlang. Writing
Total = 2 + 3 * 4 in Prolog does not compute 14 — it tries to unify Total with the unevaluated term 2+3*4, which only works if Total is already unbound (it would bind to the raw expression tree, not a number). is/2 is the operator that actually evaluates the right-hand side and unifies the result with the left.Integer division, remainder, and float division
io:format("~p ~p ~p~n", [17 div 5, 17 rem 5, 17 / 5]). :- X is 17 // 5, Y is 17 mod 5, Z is 17 / 5,
format("~w ~w ~w~n", [X, Y, Z]). Both languages separate integer and float division cleanly. Erlang spells integer division and remainder as the words
div and rem; Tau Prolog spells them as the operators // and mod. In both, the plain / always produces a float, even when the operands divide evenly.Calling into a math library
io:format("~p~n", [math:sqrt(16.0)]). :- X is sqrt(16.0),
writeln(X). Erlang routes math functions through the
math module (math:sqrt/1, math:pow/2). Prolog's arithmetic evaluator recognizes function-like terms — sqrt(16.0), pow(2, 10) — directly inside an is/2 expression, with no module prefix needed.Generating a range
Range = lists:seq(1, 5),
io:format("~p~n", [Range]). :- findall(X, between(1, 5, X), Range),
writeln(Range). Erlang's
lists:seq/2 eagerly builds the whole list [1,2,3,4,5]. Prolog's between/3 is a goal, not a list builder — on backtracking it generates one integer at a time. Wrapping it in findall/3 (see the "Collecting Solutions" section) is how you turn that generator into an ordinary list, the way lists:seq/2 hands you one immediately.Comparison
The same `=<`, for the same reason
io:format("~p ~p~n", [3 =< 4, 5 >= 5]). :- ( 3 =< 4 -> writeln(true) ; writeln(false) ),
( 5 >= 5 -> writeln(true) ; writeln(false) ). Another small but genuine comfort: both languages spell "less than or equal" as
=<, not the <= that most C-descended languages use. Erlang and Prolog share this convention because both trace back to the same academic lineage — you will not need to retrain your fingers here.Careful: `==` and `=:=` swap meanings
% Erlang: == compares VALUE (numeric coercion allowed); =:= is EXACT (type must match too)
io:format("~p ~p~n", [1 == 1.0, 1 =:= 1.0]). % Prolog: =:= evaluates and compares ARITHMETICALLY; == compares terms with NO evaluation
:- ( 1 =:= 1.0 -> writeln(true) ; writeln(false) ),
( 1 == 1.0 -> writeln(true) ; writeln(false) ). This is a genuine trap, not just a naming difference. In Erlang,
== is the loose comparison (1 == 1.0 is true) and =:= is the exact one (1 =:= 1.0 is false, different types). In Prolog the roles are inverted: =:= evaluates both sides arithmetically and compares the numbers (1 =:= 1.0 is true), while == compares the raw terms with no evaluation at all, so 1 == 1.0 is false — an integer and a float are simply different terms."Cannot unify" as its own operator
% Erlang has no direct equivalent — you would test with a case or pattern guard:
CanMatch = fun(A, B) -> A =/= B end,
io:format("~p~n", [CanMatch(foo, bar)]). :- ( foo \= bar -> writeln(true) ; writeln(false) ). Prolog's
\= asks "do these two terms fail to unify?" — it is the negation of =/2, checked without actually binding any variables. Erlang has no single operator for this because it has no unification-that-can-fail-and-be-asked-about outside of pattern matching itself; the closest equivalent is simply comparing two already-bound values with =/=.A total ordering over every term
Sorted = lists:sort([3, foo, 1.5, "bar"]),
io:format("~p~n", [Sorted]). :- msort([3, foo, 1.5, bar], Sorted),
writeln(Sorted). Both languages define a total order across every term, not just numbers, so that
lists:sort/1 (Erlang) and msort/2 (Prolog, "sort but keep duplicates") never raise a "these types are not comparable" error the way many languages would. The exact ordering rules differ in the details, but the guarantee — everything can be placed somewhere — is the same design decision.Lists
List literals look identical
Numbers = [1, 2, 3, 4, 5],
io:format("~p~n", [Numbers]). :- Numbers = [1, 2, 3, 4, 5],
writeln(Numbers). The bracket-and-comma list syntax is exactly the same character for character. Both are singly-linked cons lists under the hood, so this is not just a syntactic coincidence — the underlying data structure and its performance characteristics (cheap to prepend, linear to index) match too.
The `[Head | Tail]` pattern
[Head | Tail] = [1, 2, 3],
io:format("~p and ~p~n", [Head, Tail]). :- [Head | Tail] = [1, 2, 3],
format("~w and ~w~n", [Head, Tail]). This is the single most direct syntactic match in the whole comparison:
[Head | Tail] means the same thing, is spelled the same way, and unifies the same way in both languages. Whatever recursive list-processing instincts you have built up writing Erlang carry over completely unchanged.Testing and generating membership
io:format("~p~n", [lists:member(3, [1, 2, 3])]). :- ( member(3, [1, 2, 3]) -> writeln(true) ; writeln(false) ). Even the name matches:
lists:member/2 in Erlang and member/2 in Prolog. The deeper difference only shows up when the first argument is unbound — Erlang's version cannot do that, but Prolog's member(X, [1,2,3]) becomes a generator that produces X = 1, then X = 2, then X = 3 on backtracking.Concatenating lists
io:format("~p~n", [lists:append([1, 2], [3, 4])]). :- append([1, 2], [3, 4], Result),
writeln(Result). Erlang's
lists:append/2 takes two lists and returns their concatenation, the way you would expect from any language. Prolog's append/3 is a genuine relation between three lists, and — unlike Erlang's version — it works in reverse too: append(X, Y, [1,2,3]) backtracks through every way to split [1,2,3] into a prefix and a suffix.Reversing a list
io:format("~p~n", [lists:reverse([1, 2, 3])]). :- reverse([1, 2, 3], Result),
writeln(Result). Same idea, same name, one small shape difference: Erlang's
lists:reverse/1 returns its result, while Prolog's reverse/2 takes the input list and an output variable to unify with the reversed list — the general Prolog pattern of "one extra argument for the answer" that shows up throughout the standard library.Recursion
Computing length by hand
Length = fun
Length([]) -> 0;
Length([_ | Tail]) -> 1 + Length(Tail)
end,
io:format("~p~n", [Length([a, b, c, d])]). my_length([], 0).
my_length([_ | Tail], Length) :-
my_length(Tail, TailLength),
Length is TailLength + 1.
:- my_length([a, b, c, d], Length),
writeln(Length). The recursive structure is identical: a base case for the empty list, and a recursive case that peels off the head and recurses on the tail. The visible difference is where the answer lives — Erlang's recursive
fun returns a value, while the Prolog rule threads the answer through an extra argument (Length) that gets unified once the recursion bottoms out.Summing a list recursively
SumList = fun
SumList([]) -> 0;
SumList([Head | Tail]) -> Head + SumList(Tail)
end,
io:format("~p~n", [SumList([1, 2, 3, 4, 5])]). sum_list_manual([], 0).
sum_list_manual([Head | Tail], Sum) :-
sum_list_manual(Tail, TailSum),
Sum is Head + TailSum.
:- sum_list_manual([1, 2, 3, 4, 5], Sum),
writeln(Sum). Both versions decompose the list the same way. Note the Prolog rule must say
Sum is Head + TailSum rather than Sum = Head + TailSum — a reminder from the arithmetic section that unification alone will not add two numbers together; only is/2 evaluates.Factorial
Factorial = fun
Factorial(0) -> 1;
Factorial(N) when N > 0 -> N * Factorial(N - 1)
end,
io:format("~p~n", [Factorial(5)]). factorial(0, 1).
factorial(N, Result) :-
N > 0,
N1 is N - 1,
factorial(N1, SubResult),
Result is N * SubResult.
:- factorial(5, Result),
writeln(Result). The classic recursive definition transfers almost line for line: a base case, a guard restricting the recursive case to positive numbers, and a recursive call whose result feeds the final multiplication. Erlang's
when N > 0 guard and Prolog's bare N > 0 goal in the body serve the identical purpose.Fibonacci
Fib = fun
Fib(0) -> 0;
Fib(1) -> 1;
Fib(N) when N > 1 -> Fib(N - 1) + Fib(N - 2)
end,
io:format("~p~n", [Fib(10)]). fib(0, 0).
fib(1, 1).
fib(N, Result) :-
N > 1,
N1 is N - 1,
N2 is N - 2,
fib(N1, Result1),
fib(N2, Result2),
Result is Result1 + Result2.
:- fib(10, Result),
writeln(Result). Three clauses, two base cases, one recursive case — the shape is identical in both languages, right down to using multiple clauses (rather than a single one with an
if) to separate the cases. This is a good example of how naturally Erlang's "many small clauses" style maps onto Prolog's "many small rules."Atoms & Strings
A double-quoted literal is a list of codes — in both
% In Erlang, "abc" is a LIST of character codes:
Value = "abc",
io:format("~w~n", [Value]). % ~w shows the raw list: [97,98,99] % In (this) Prolog, a bare atom quoted with '...' is the everyday string type;
% double quotes also produce a code list, just as in Erlang.
:- Value = 'abc',
writeln(Value). Erlang's
"abc" is secretly [97, 98, 99], a plain list of integer code points — which is why ~w ("write the raw term") shows the numbers, while the everyday ~p/~s directives print it back as text. Prolog inherited the very same double-quoted-string-is-a-code-list convention from its own history; day-to-day Prolog code more often reaches for single-quoted atoms ('abc') to hold text, which is the closest thing to Erlang's binaries or Elixir's strings.Building an atom from parts
Full = list_to_atom("Hello" ++ ", " ++ "Ada" ++ "!"),
io:format("~p~n", [Full]). :- atom_concat('Hello, ', 'Ada', Partial),
atom_concat(Partial, '!', Full),
writeln(Full). Erlang concatenates character lists with
++ and converts the final result back to an atom with list_to_atom/1 if an atom is what you actually want. Prolog's atom_concat/3 joins two atoms directly into a third — no intermediate list representation to manage.Measuring length
io:format("~p~n", [length("hello")]). :- atom_length(hello, Length),
writeln(Length). Since
"hello" is really a list in Erlang, the ordinary length/1 function measures it. Prolog's atoms are not lists, so text length needs its own predicate, atom_length/2, which unifies its second argument with the count.Converting between atoms and character lists
Chars = atom_to_list(hello),
io:format("~p~n", [Chars]). :- atom_chars(hello, Chars),
writeln(Chars). Erlang's
atom_to_list/1 and Prolog's atom_chars/2 both explode an atom into its individual characters — Erlang as a list of character codes, Prolog as a list of single-character atoms ([h,e,l,l,o]). Either direction, this is the escape hatch for treating "opaque" atom text as ordinary, inspectable list data.Backtracking & Control
The big paradigm shift: many solutions
% Erlang functions have exactly one outcome per call — there is no
% built-in notion of "give me the next answer":
Result = lists:member(2, [1, 2, 3]),
io:format("~p~n", [Result]). % Prolog can produce every solution on demand — findall/3 collects them all:
:- findall(X, member(X, [1, 2, 3]), All),
writeln(All). This is the concept that reorganizes everything else. An Erlang function call has exactly one result (or it crashes) —
lists:member/2 is a boolean test, full stop. A Prolog goal like member(X, [1,2,3]) is not a test at all; it is a generator that can be asked, again and again, for its next solution. findall/3 is one of several ways to say "give me every one of them, right now, as a list."The cut: pruning choices Erlang never had
% Erlang guards already commit to the first matching clause — there is
% nothing extra to prune; this IS Erlang's normal behavior:
Classify = fun
Classify(N) when N < 0 -> negative;
Classify(0) -> zero;
Classify(_) -> positive
end,
io:format("~p~n", [Classify(5)]). classify(N, negative) :- N < 0, !.
classify(0, zero) :- !.
classify(_, positive).
:- classify(5, Result),
writeln(Result). Erlang never needs a "cut" because its clause selection is already deterministic — one clause wins, end of story. Prolog's clause selection is not automatically final; without the
! ("cut") operator, backtracking into classify/2 later could still try the remaining clauses even after one already succeeded. The cut is Prolog reaching for the commitment that Erlang gives you for free.If/then/else
Age = 20,
Category = if
Age >= 18 -> adult;
true -> minor
end,
io:format("~p~n", [Category]). :- Age = 20,
( Age >= 18 -> Category = adult ; Category = minor ),
writeln(Category). Erlang's
if expression and Prolog's ( Cond -> Then ; Else ) construct both commit to exactly one branch and never backtrack into the other — this is the one place Prolog voluntarily gives up its "try everything" nature to behave like an ordinary conditional, which is exactly the behavior an Erlang developer already expects by default.`once/1`: forcing a single answer
% Erlang calls are always single-answer, so there is nothing to opt into —
% this IS how every Erlang function call already behaves:
FirstMatch = lists:nth(1, [X || X <- [1, 2, 3, 4], X > 1]),
io:format("~p~n", [FirstMatch]). :- once(member(X, [1, 2, 3, 4])),
writeln(X). once/1 wraps a goal so it commits to its first solution and discards the rest, behaving exactly like an ordinary Erlang function call for that one goal. Reach for it whenever you want a query to feel like calling a ordinary deterministic Erlang function rather than a search.Negation as failure
IsMissing = fun(Value, List) -> not lists:member(Value, List) end,
io:format("~p~n", [IsMissing(4, [1, 2, 3])]). :- ( \+ member(4, [1, 2, 3]) -> writeln(true) ; writeln(false) ). Erlang's
not/1 is ordinary boolean negation over a true/false result. Prolog's \+ is subtler: it means "this goal has no solutions" — it works by attempting the goal and succeeding only if that attempt fails entirely, which is why it is called negation as failure rather than logical negation. In this example the two behave the same, but \+ can give surprising answers when its argument goal contains unbound variables, since it can never bind anything itself.I/O
`write` and the newline
io:format("~s", ["no newline here"]),
io:format("~n"). :- write('no newline here'), nl. Both languages separate "print the term" from "print a newline" into two composable pieces. Erlang spells the newline as the
~n format directive; Prolog spells it as its own predicate, nl/0. writeln/1 (used throughout this cheatsheet) is simply write/1 followed by nl/0, bundled for convenience.Formatted output with placeholders
io:format("~p is ~p years old~n", [ada, 36]). :- format("~w is ~w years old~n", [ada, 36]). The naming coincidence continues right into formatted output: both languages call it
format, both take a template string with placeholders and a list of values, and both use ~n for a newline. Erlang's ~p ("pretty print") corresponds most closely to Prolog's ~w ("write") here — Prolog's format/2 comes from the library(format) module rather than the core language, the way Erlang's comes from the io module.Printing each element of a list
lists:foreach(fun(N) -> io:format("~p~n", [N]) end, [1, 2, 3]). :- forall(member(N, [1, 2, 3]), (write(N), nl)). Erlang's
lists:foreach/2 and Prolog's forall/2 both walk a collection purely for side effects, discarding any "return value." forall(Condition, Action) reads naturally as "for every way Condition can be true, do Action" — here, "for every member of the list, print it."Higher-Order
`maplist/2`: a goal applied to every element
lists:foreach(fun(N) -> io:format("~p ", [N * N]) end, [1, 2, 3]),
io:format("~n"). print_square(N) :- Squared is N * N, write(Squared), write(' ').
:- maplist(print_square, [1, 2, 3]),
nl. Prolog's
maplist/2 applies a goal to every element of a list, checking that the goal succeeds for each — the closest analogue to Erlang's lists:foreach/2. Where Erlang can build an anonymous fun inline, ordinary Prolog reaches for a small named predicate like print_square/1 to pass around as the goal — Tau Prolog does not implement the library(yall) lambda extension that some other Prolog systems offer.`maplist/3`: transforming into a new list
Doubled = lists:map(fun(N) -> N * 2 end, [1, 2, 3]),
io:format("~p~n", [Doubled]). double(N, Doubled) :- Doubled is N * 2.
:- maplist(double, [1, 2, 3], Result),
writeln(Result). Erlang's
lists:map/2 returns a new list directly. Prolog's maplist/3 takes a two-argument relation (here double/2) and two lists, and holds them related element-by-element — the output list Result is unified in place rather than returned, the same "extra argument for the answer" shape you have already seen in reverse/2 and append/3.Calling a goal built at runtime
Function = fun(X) -> X + 1 end,
io:format("~p~n", [Function(41)]). :- Goal = succ,
call(Goal, 41, Result),
writeln(Result). Erlang can hold a
fun in a variable and invoke it directly, or reach for apply/3 when the function and arguments are assembled dynamically. Prolog's call/N is the direct analogue of apply/3: it takes a goal (here the atom succ, naming the built-in successor predicate) and appends extra arguments to it before executing.Collecting Solutions
`findall/3` is a list comprehension
Evens = [X || X <- lists:seq(1, 10), X rem 2 =:= 0],
io:format("~p~n", [Evens]). :- findall(X, (between(1, 10, X), X mod 2 =:= 0), Evens),
writeln(Evens). These are the same idea wearing different syntax. Erlang's
[X || X <- Source, Condition] and Prolog's findall(X, (Generator, Condition), List) both say "collect every X produced by this generator that also satisfies this condition" — the Erlang generator clause and the Prolog goal argument play matching roles.`bagof/3`: fails instead of returning empty
% Erlang: an empty list comprehension is completely unremarkable —
Results = [X || X <- [], X > 100],
io:format("~p~n", [Results]). :- ( bagof(X, member(X, []), Results)
-> writeln(Results)
; writeln('bagof fails on no solutions') ). A list comprehension over nothing quietly produces
[] in Erlang — an entirely unremarkable value. bagof/3 makes a different choice: when the generator has zero solutions, the whole goal fails rather than binding an empty list. That distinction — "no answers" versus "the answer is empty" — matters more in Prolog because failure is itself a control-flow signal, not just an edge case to check for.`setof/3`: sorted and deduplicated
Unique = lists:usort([3, 1, 2, 1, 3]),
io:format("~p~n", [Unique]). :- setof(X, member(X, [3, 1, 2, 1, 3]), Unique),
writeln(Unique). Erlang's
lists:usort/1 sorts a list and removes duplicates in one call. setof/3 gives Prolog the same guarantee — sorted, deduplicated results — but as a byproduct of collecting solutions rather than a standalone list function; like bagof/3, it fails outright if there are no solutions to collect.Summing collected solutions
Total = lists:sum([X * X || X <- lists:seq(1, 5)]),
io:format("~p~n", [Total]). :- findall(Squared, (between(1, 5, X), Squared is X * X), Squares),
sum_list(Squares, Total),
writeln(Total). This is the common Prolog idiom for what a Rubyist or Erlang developer would reach for a single
map-then-reduce pipeline to do: findall/3 plays the role of map (collecting the transformed values), and sum_list/2 plays the role of reduce. Erlang's list comprehension plus lists:sum/1 does the equivalent work in a single expression.Mutable State vs. the Dynamic Database
Adding a fact while the program runs
% Erlang has no mutable "fact base" — new facts become new list entries,
% and you must thread the updated list back to wherever it is needed:
People = [alice, bob],
UpdatedPeople = People ++ [carol],
io:format("~p~n", [UpdatedPeople]). :- dynamic(person/1).
person(alice).
person(bob).
:- assertz(person(carol)),
findall(Who, person(Who), All),
writeln(All). Here the two paradigms visibly part ways. Erlang has no mutable storage at all — "adding a fact" means building a new list and passing it along; the old list is untouched and still exists. Prolog's
assertz/1 genuinely mutates the running program's database in place, appending a new clause that every subsequent query can see. A predicate must be declared :- dynamic(person/1). before it can be asserted into, since ordinarily Prolog treats its clause database as fixed once loaded.Removing a fact
% Erlang: removal means filtering to a new list, again with no mutation:
People = [alice, bob, carol],
Remaining = lists:filter(fun(Person) -> Person =/= bob end, People),
io:format("~p~n", [Remaining]). :- dynamic(person/1).
person(alice).
person(bob).
person(carol).
:- retract(person(bob)),
findall(Who, person(Who), Remaining),
writeln(Remaining). retract/1 is the inverse of assertz/1: it deletes a clause matching the given pattern from the live database. There is nothing resembling this in Erlang, on purpose — Erlang's entire fault-tolerance story (the "let it crash" philosophy) depends on data never silently changing out from under a running process.A counter: process state vs. the database
% Erlang models a mutable counter as a recursive process holding state
% in its own arguments — no global variable exists to mutate:
Counter = fun Counter(Count) ->
io:format("~p~n", [Count]),
Counter(Count + 1)
end,
% (calling Counter(0) would loop forever in a real program; a single
% simulated step is shown here instead)
io:format("~p~n", [0]). :- dynamic(counter/1).
counter(0).
increment :-
retract(counter(Count)),
NewCount is Count + 1,
assertz(counter(NewCount)).
:- increment,
increment,
counter(Count),
writeln(Count). This contrast gets at something fundamental. To keep mutable state, idiomatic Erlang spawns a process that recurses, carrying the current value as an argument to its own next invocation — state lives in the call stack of a loop, never in a shared variable. Prolog's
assertz/retract pair offers a shortcut: a genuinely mutable global fact, updated directly. It is convenient, but it is also the one place Prolog's "pure logic" story quietly breaks down — much the way Erlang's process-based state is a deliberate workaround for its own purity, not an exception to it.Classic Examples
A family tree, queried both ways
Parents = [{tom, bob}, {tom, liz}, {bob, ann}, {bob, pat}],
Grandparent = fun(GrandChild) ->
[Grandparent || {Grandparent, Parent} <- Parents,
{P2, C} <- Parents,
Parent =:= P2,
C =:= GrandChild]
end,
io:format("~p~n", [Grandparent(ann)]). parent(tom, bob).
parent(tom, liz).
parent(bob, ann).
parent(bob, pat).
grandparent(Grandparent, GrandChild) :-
parent(Grandparent, Parent),
parent(Parent, GrandChild).
:- findall(G, grandparent(G, ann), Grandparents),
writeln(Grandparents). The Erlang version needs a nested list comprehension to join the "parent of a parent" relationship by hand. The Prolog
grandparent/2 rule states the relationship once, declaratively, and Prolog handles the two-step join internally every time it is queried — this is the classic example that shows why Prolog is called a relational language.Finding a path through a graph
Edges = [{a, b}, {b, c}, {c, d}],
PathExists = fun PathExists(From, To) when From =:= To -> true;
PathExists(From, To) ->
lists:any(fun({F, Next}) -> (F =:= From) andalso PathExists(Next, To) end, Edges)
end,
io:format("~p~n", [PathExists(a, d)]). edge(a, b).
edge(b, c).
edge(c, d).
path(From, From).
path(From, To) :-
edge(From, Next),
path(Next, To).
:- ( path(a, d) -> writeln(true) ; writeln(false) ). Depth-first graph traversal is where Prolog's built-in backtracking earns its keep. The Erlang version has to write out the "try each outgoing edge, recurse, and stop at the first success" logic explicitly with
lists:any/2. The Prolog version states two facts about what a path is (an empty path from a node to itself, or an edge followed by a shorter path) and lets backtracking supply the depth-first search for free.Finding the minimum
Minimum = fun
Minimum([Single]) -> Single;
Minimum([Head | Tail]) ->
TailMin = Minimum(Tail),
case Head < TailMin of
true -> Head;
false -> TailMin
end
end,
io:format("~p~n", [Minimum([5, 2, 8, 1, 9])]). list_min([Single], Single).
list_min([Head | Tail], Min) :-
list_min(Tail, TailMin),
( Head < TailMin -> Min = Head ; Min = TailMin ).
:- list_min([5, 2, 8, 1, 9], Min),
writeln(Min). Recursive minimum-finding is a good final gut check on how closely the two languages' recursive style aligns: a one-element base case, a recursive case that compares the head against the minimum of everything after it, and an ordinary conditional to pick the smaller. Line for line, this could almost be a mechanical translation.
Tail-recursive reverse with an accumulator
Reverse = fun
Reverse([], Accumulator) -> Accumulator;
Reverse([Head | Tail], Accumulator) -> Reverse(Tail, [Head | Accumulator])
end,
io:format("~p~n", [Reverse([1, 2, 3, 4], [])]). reverse_accumulator([], Accumulator, Accumulator).
reverse_accumulator([Head | Tail], Accumulator, Result) :-
reverse_accumulator(Tail, [Head | Accumulator], Result).
:- reverse_accumulator([1, 2, 3, 4], [], Result),
writeln(Result). An accumulator parameter that carries the "answer so far" is a habit every Erlang developer already has for writing tail-recursive, constant-stack-space functions. Prolog rewards the exact same habit for the exact same reason: threading an accumulator through the recursive calls keeps each call a proper tail call, which Tau Prolog (like Erlang's BEAM) can optimize.