PONY λ M2 Modula-2

Haskell.CodeCompared.To/Prolog

An interactive executable cheatsheet comparing Haskell and Prolog

GHC 9.12 Tau Prolog 0.3
Output & Running
Hello, World
main :: IO () main = putStrLn "Hello, World!"
:- writeln('Hello, World!').
Haskell has exactly one entry point, main :: IO (), which the runtime calls once. Prolog has no single entry point at all — a program runs as a sequence of :- Goal. directives, each executed the moment it is read, in file order. writeln/1 writes a term followed by a newline, the closest one-line analogue of putStrLn.
Line comments look completely different
-- This is a Haskell comment, running to end of line. main :: IO () main = print "ok"
% This is a Prolog comment, running to end of line. :- writeln(ok).
Haskell uses -- for a line comment (and {- ... -} for a block comment); Prolog uses % for a line comment (and also supports C-style /* block comments */). Neither symbol transfers from the other language, so this is one habit that has to be relearned from scratch.
Printing a value versus printing a term
data Point = Point Int [Int] deriving Show main :: IO () main = print (Point 1 [2, 3])
:- Value = point(1, [2, 3]), write(Value), nl.
Haskell's print relies on a Show instance — usually deriving Show — so the compiler generates the rendering code for you at compile time, and only types with such an instance can be printed. Prolog's write/1 needs no such declaration: any term at all, including the compound term point(1, [2, 3]), prints in its functor-then-arguments form because Prolog has no static type system to consult in the first place.
Facts & Queries
Facts replace a list of values
people :: [String] people = ["alice", "bob", "carol"] main :: IO () main = mapM_ (\person -> putStrLn (person ++ " is a person")) people
person(alice). person(bob). person(carol). :- forall(person(Who), (write(Who), writeln(' is a person'))).
A Haskeller's first instinct is to model "alice is a person" as membership in a list, then walk it with mapM_. Prolog instead gives a relation first-class syntax: person(alice). declares something true directly, as a standalone fact, and forall/2 runs a goal once for every solution of its first argument — the logic-programming equivalent of mapM_ over "every fact that matches".
Nullary constructors already feel like atoms
data Status = Ok | NotFound | Error deriving (Show, Eq) describe :: Status -> String describe Ok = "ok is a status" describe NotFound = "not found" describe Error = "an error" main :: IO () main = putStrLn (describe Ok)
status(ok). status(not_found). status(error). :- ( status(ok) -> writeln('ok is a status') ; writeln('not a status') ).
A Haskeller already has a head start most other newcomers lack: a nullary data constructor like Ok and a Prolog atom like ok both act as bare symbolic tags. The crucial difference is that Ok is one value of the closed, compiler-checked type Status, while ok is just a name — Prolog attaches no type at all, so nothing stops you from writing status(42) right alongside it.
Finding every match
likes :: [(String, String)] likes = [("alice", "books"), ("alice", "tea"), ("bob", "coffee")] main :: IO () main = mapM_ putStrLn [thing | (person, thing) <- likes, person == "alice"]
likes(alice, books). likes(alice, tea). likes(bob, coffee). :- forall(likes(alice, Thing), writeln(Thing)).
The Haskell side needs an explicit list comprehension with a guard to filter down to Alice's rows — you are telling the machine how to search a list you already built. The query likes(alice, Thing) asks Prolog "for what values of Thing is this true?" and the engine searches its own fact database and finds every match on its own — you describe what, not how.
Capitalization means the opposite thing
-- Haskell: lowercase-first = variable or function; uppercase-first = type or data constructor. main :: IO () main = let x = "alice" in putStrLn x
% Prolog: the reverse convention — uppercase-first = variable; lowercase-first = atom. :- X = alice, write('X is '), writeln(X).
This is a genuine trap for a Haskeller, not just a naming quirk. In Haskell, a name starting lowercase is a variable or function (x, describe), and a name starting uppercase is a type or data constructor (Status, Ok) — the compiler enforces this. Prolog's rule is the mirror image: uppercase-first names are variables, and lowercase-first names are atoms. Muscle memory from Haskell will misfire constantly here until it is deliberately retrained.
Unification & Pattern Matching
Pattern matching is one-directional; unification is not
data Point = Point Int Int main :: IO () main = let Point x y = Point 1 2 in putStrLn (show x ++ " and " ++ show y)
:- point(X, Y) = point(1, 2), format("~w and ~w~n", [X, Y]).
Haskell pattern matching only ever flows one way: a known value on the right is destructured against a pattern on the left, binding the pattern's variables. Prolog's =/2 is genuinely bidirectional unification — either side can contain unbound variables, and the engine solves for whichever bindings make both sides identical. Here it looks the same because the right side is fully known, but that symmetry is not guaranteed the way it is in Haskell.
A failed match crashes in Haskell, but is quiet in Prolog
data Result = Ok Int | Err String main :: IO () main = -- Ok value <- Err "not found" would crash at runtime: "Non-exhaustive patterns". let Ok value = Ok 42 in print 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 a pattern genuinely cannot match, the two languages diverge on how loud the failure is. An incomplete Haskell pattern match — one lacking a case for the value actually received — throws a runtime exception, "Non-exhaustive patterns in function", which the compiler can warn about but not always prevent. A failed Prolog unification is just an ordinary, quiet false: the goal fails, and if the engine has other choices left to try (see backtracking, below), it moves on to them instead of crashing anything.
The wildcard `_` means the same thing in both
main :: IO () main = let (_, second, _) = ("first", "keep me", "third") in putStrLn second
:- point(_, Second, _) = point(first, 'keep me', third), writeln(Second).
A rare exact match: the underscore _ means "match anything here, and do not bother binding a name" in both languages, and neither one warns about an unused _ the way it would warn about an unused named binding.
Nested structures match in one motion
main :: IO () main = let Just (first : rest) = Just [1, 2, 3] in putStrLn (show first ++ " then " ++ show rest)
:- Result = ok([1, 2, 3]), Result = ok([First | Rest]), format("~w then ~w~n", [First, Rest]).
Both languages recurse through nested structure to bind variables in one pass — a wrapped cons list is destructured element by element, with first/First and rest/Rest bound simultaneously. The x : xs cons-cell pattern you already write daily in Haskell is, modulo the bracket syntax, exactly the shape of Prolog's [First | Rest].
Rules vs. Equations
A rule reads like a guarded equation
isAdult :: Int -> Bool isAdult age = age >= 18 main :: IO () main = print (isAdult 20)
is_adult(Age) :- Age >= 18. :- ( is_adult(20) -> writeln(true) ; writeln(false) ).
A Prolog rulehead :- body. — reads "head is true if body is true," which is the same relationship a Haskell equation expresses between its arguments and its (guarded) right-hand side. The difference is that a rule is a logical statement you can query, backtrack through, and even run in reverse, not merely a computation you call for its return value.
Conjunction: comma means "and"
canVote :: Int -> Bool -> Bool canVote age registered = age >= 18 && registered main :: IO () main = print (canVote 20 True)
can_vote(Age, Registered) :- Age >= 18, Registered == true. :- ( can_vote(20, true) -> writeln(true) ; writeln(false) ).
The comma inside a Prolog rule body means "and," playing the same role as Haskell's &&. The crucial difference is that each comma-separated Prolog goal can itself backtrack into multiple solutions — a chain of Haskell && expressions only ever evaluates to a single True or False.
Disjunction: two ways to succeed
isWeekend :: String -> Bool isWeekend day = day == "sat" || day == "sun" main :: IO () main = print (isWeekend "sun")
is_weekend(sat). is_weekend(sun). :- ( is_weekend(sun) -> writeln(true) ; writeln(false) ).
Haskell's || is a boolean short-circuit inside one equation. Idiomatic 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 matches. This is also how Prolog handles what Haskell would call separate equations for different constructor patterns.
Multiple clauses, chosen by matching — but who commits?
describe :: Int -> String describe 0 = "zero" describe n | n > 0 = "positive" | otherwise = "negative" main :: IO () main = putStrLn (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 — | n > 0 and the Prolog body goal N > 0 play the same role. The difference surfaces on backtracking: once a Haskell equation's guard succeeds, that choice is final. If a Prolog goal later in the program forces backtracking, the engine can return to describe/2 and try a clause it skipped, something Haskell's deterministic equations never do.
Arithmetic
Arithmetic evaluation needs `is` in Prolog
-- Haskell evaluates arithmetic directly wherever an expression is expected: main :: IO () main = print (2 + 3 * 4)
% 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 a Haskeller. In Haskell, 2 + 3 * 4 is simply an expression that evaluates to 14 the moment it is forced — that is what a pure functional language does by design. Writing Total = 2 + 3 * 4 in Prolog does not compute anything: =/2 tries to unify Total with the raw, unevaluated term 2+3*4. 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
main :: IO () main = print (17 `div` 5, 17 `mod` 5, fromIntegral (17 :: Int) / 5 :: Double)
:- X is 17 // 5, Y is 17 mod 5, Z is 17 / 5, format("~w ~w ~w~n", [X, Y, Z]).
Both languages keep integer and float division cleanly separate, and even the names are close cousins: Haskell spells integer division and remainder as the infix functions div and mod; Tau Prolog spells them // and mod — the modulus keyword transfers unchanged. In both languages the plain / always produces a floating-point result.
Calling into a math library
main :: IO () main = print (sqrt 16.0)
:- X is sqrt(16.0), writeln(X).
Haskell's sqrt is an ordinary function from the Floating typeclass, imported implicitly via the Prelude. Prolog's arithmetic evaluator recognizes function-like terms — sqrt(16.0), pow(2, 10) — directly inside an is/2 expression, with no import or module prefix needed either way.
Comparison
`<=` becomes `=<`
main :: IO () main = print (3 <= 4, 5 >= 5)
:- ( 3 =< 4 -> writeln(true) ; writeln(false) ), ( 5 >= 5 -> writeln(true) ; writeln(false) ).
Haskell spells "less than or equal" the ordinary <= way, matching most languages. Prolog is one of the few that reorders it to =< — a small but genuine finger-retraining exercise, since <= is not valid Prolog syntax for this operator at all.
One `==` in Haskell splits into two in Prolog
main :: IO () main = print ((1 :: Double) == 1.0, (1 :: Int) == 1)
% Prolog: =:= evaluates and compares ARITHMETICALLY; == compares terms with NO evaluation :- ( 1 =:= 1.0 -> writeln(true) ; writeln(false) ), ( 1 == 1.0 -> writeln(true) ; writeln(false) ).
In Haskell, == is a single typeclass method, and the type system already prevents comparing an Int to a Double without an explicit conversion, so there is only ever one kind of equality to reach for. Prolog has no such static check, so it must split the job in two: =:= evaluates both sides arithmetically and compares the resulting 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 to Prolog.
A total ordering over every term, without a typeclass
data AnyValue = AnInt Int | AnAtom String | ADouble Double deriving (Show, Eq, Ord) main :: IO () main = print [AnInt 3, AnAtom "foo", ADouble 1.5]
:- msort([3, foo, 1.5, bar], Sorted), writeln(Sorted).
To compare values of genuinely different shapes in Haskell, you must first unify them under one algebraic data type and derive Ord for it — the compiler enforces that comparison is always type-directed. Prolog defines a single standard order of terms that spans every value at once — numbers, atoms, and compounds all sit on one universal scale — so msort/2 ("sort but keep duplicates") never needs a type declaration to sort a mixed list like [3, foo, 1.5, bar].
Lists
List literals look nearly identical
main :: IO () main = print [1, 2, 3, 4, 5]
:- Numbers = [1, 2, 3, 4, 5], writeln(Numbers).
The bracket-and-comma list syntax is character for character the same. 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 cons pattern, spelled two different ways
describeList :: [Int] -> String describeList [] = "empty" describeList (x : xs) = show x ++ " followed by " ++ show xs main :: IO () main = putStrLn (describeList [1, 2, 3])
describe_list([]) :- writeln(empty). describe_list([Head | Tail]) :- format("~w followed by ~w~n", [Head, Tail]). :- describe_list([1, 2, 3]).
Haskell's x : xs cons pattern and Prolog's [Head | Tail] are the same idea with different punctuation — a colon versus a pipe — both splitting a nonempty list into its first element and the rest. Both languages also give the empty list its own separate pattern, [], that must be matched explicitly.
Appending two lists
main :: IO () main = print ([1, 2] ++ [3, 4])
:- append([1, 2], [3, 4], Result), writeln(Result).
Haskell's ++ is a function that always runs forward: two known lists in, one new list out. Prolog's append/3 is a true relation between three lists — called with the first two bound, it behaves like ++, but called with the third bound and the first two left as variables, it instead finds every way to split the third list, something ++ has no way to express at all.
Membership as a query, not just a check
main :: IO () main = print (3 `elem` [1, 2, 3, 4])
:- ( member(3, [1, 2, 3, 4]) -> writeln(true) ; writeln(false) ). :- findall(X, member(X, [1, 2, 3, 4]), All), writeln(All).
elem only ever answers True or False. Prolog's member/2 can be used the same way, but it is really a generator: called with the first argument unbound, member(X, [1, 2, 3, 4]) produces every element of the list on successive backtracks, which findall/3 (see "Collecting Solutions") can gather back into an ordinary list.
Reversing a list
main :: IO () main = print (reverse [1, 2, 3])
:- reverse([1, 2, 3], Result), writeln(Result).
The names match exactly, and so does the shape of the call in spirit — Haskell's reverse takes the list and returns the reversed one; Prolog's reverse/2 takes the list and unifies its second argument with the reversed result, the "extra argument for the answer" pattern that recurs throughout Prolog's list-processing built-ins.
Recursion
Factorial by structural recursion
factorial :: Integer -> Integer factorial 0 = 1 factorial n = n * factorial (n - 1) main :: IO () main = print (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).
Both definitions read the same way: a base case for zero, and a recursive case that reduces the problem by one step and combines the result. The Prolog version needs an explicit extra argument to carry the answer back out, since a Prolog predicate has no return value in the Haskell sense — every "result" is just another argument the caller unifies with.
The accumulator pattern, in both languages
sumList :: [Int] -> Int -> Int sumList [] accumulator = accumulator sumList (x : xs) accumulator = sumList xs (accumulator + x) main :: IO () main = print (sumList [1, 2, 3, 4] 0)
sum_with_accumulator([], Accumulator, Accumulator). sum_with_accumulator([Head | Tail], Accumulator, Result) :- NewAccumulator is Accumulator + Head, sum_with_accumulator(Tail, NewAccumulator, Result). :- sum_with_accumulator([1, 2, 3, 4], 0, Result), writeln(Result).
The tail-recursive accumulator idiom transfers unchanged: an extra parameter carries the running total forward instead of building it up on the way back out of the recursion, which keeps both the Haskell and the Prolog version constant-stack (modulo each runtime's own tail-call handling).
Defining `length` yourself, then trusting the built-in
myLength :: [a] -> Int myLength [] = 0 myLength (_ : xs) = 1 + myLength xs main :: IO () main = print (myLength [1, 2, 3, 4, 5], length [1, 2, 3, 4, 5])
my_length([], 0). my_length([_ | Tail], Result) :- my_length(Tail, TailLength), Result is TailLength + 1. :- my_length([1, 2, 3, 4, 5], Custom), length([1, 2, 3, 4, 5], Builtin), format("~w ~w~n", [Custom, Builtin]).
Writing length by hand once is a useful exercise in both languages precisely because the real built-ins — Haskell's length and Prolog's length/2 — do the identical recursive walk internally, just more efficiently. Note that Prolog's length/2 can also run backwards: given a bound second argument and an unbound list, it generates a list of that many fresh variables.
Atoms & Strings
An atom carries no type, unlike a constructor
data Status = Ok | NotFound deriving (Show, Eq) main :: IO () main = print (Ok == Ok)
:- ( ok == ok -> writeln(true) ; writeln(false) ).
A Haskell nullary constructor like Ok belongs to a specific, closed, compiler-checked type — you cannot accidentally compare it to a value of an unrelated type. A Prolog atom like ok is just a symbolic name with no type attached at all; the same atom can be used as a status tag in one predicate and a completely unrelated meaning in another, with nothing enforcing consistency.
Concatenation: `++` versus `atom_concat/3`
main :: IO () main = putStrLn ("Hello, " ++ "World!")
:- atom_concat('Hello, ', 'World!', Result), writeln(Result).
Haskell's ++ concatenates any list, strings included, since a String is just [Char]. Prolog's atom_concat/3 is specific to atoms, and like append/3 it is a true relation: called with the third argument bound and the first two left as variables, it can instead search for every way to split one atom into two.
`show` is type-directed; `write` is not
data Shape = Circle Double | Square Double deriving Show main :: IO () main = putStrLn (show (Circle 2.5))
:- Value = circle(2.5), write(Value), nl.
Haskell's show comes from the Show typeclass, and deriving Show asks the compiler to generate a rendering specific to that exact type's shape at compile time. Prolog's write/1 needs no such declaration or derivation — it prints whatever term it is given, functor and arguments, because printing is a runtime operation over untyped terms rather than a typeclass method resolved per type.
Backtracking & Control
`if` is a pure expression; `->` is a goal
main :: IO () main = let age = 20 in putStrLn (if age >= 18 then "adult" else "minor")
:- Age = 20, ( Age >= 18 -> writeln(adult) ; writeln(minor) ).
Haskell's if is a pure expression: both branches must exist and both must have the same type, because the whole thing must evaluate to a single value. Prolog's ( Cond -> Then ; Else ) is built from goals, not values — dropping the ; Else entirely is legal Prolog (the whole construct simply fails if Cond is false), something Haskell's type checker would never allow.
Two different guarantees against "the wrong case ran"
classify :: Int -> String classify n | n < 0 = "negative" | n == 0 = "zero" | otherwise = "positive" main :: IO () main = putStrLn (classify 5)
classify(N, negative) :- N < 0, !. classify(N, zero) :- N =:= 0, !. classify(_, positive). :- classify(5, Result), writeln(Result).
Haskell's guarded equations are exhaustively checked (with a warning, at least) at compile timeotherwise is really just True, a catch-all the compiler can reason about statically. Prolog has no compile-time exhaustiveness checking at all; the cut ! is a runtime commitment device that prunes away the choice points for earlier, already-tried clauses, preventing backtracking from re-trying classify(5, zero) after classify(5, negative) has already committed to failing past its guard.
Negation as failure versus `Maybe`
import Data.List (find) main :: IO () main = case find (== 99) [1, 2, 3] of Nothing -> putStrLn "not found" Just _ -> putStrLn "found"
person(alice). person(bob). :- ( \+ person(carol) -> writeln('not found') ; writeln(found) ).
Haskell forces the absence of a value to be handled explicitly and statically through Maybe — the compiler will not let you forget the Nothing case. Prolog's \+ Goal ("negation as failure") instead asks, at runtime, whether a goal has no solution at all; it is a much weaker guarantee, since it only knows what it could not prove within its own closed database, not whether something is genuinely, provably false.
I/O
Sequencing actions: `do` versus a comma chain
main :: IO () main = do putStrLn "Starting" putStrLn "Working" putStrLn "Done"
:- writeln('Starting'), writeln('Working'), writeln('Done').
Haskell's do-notation is syntactic sugar over chained >>/>>= calls in the IO monad — each line is really one expression built from the last. Prolog needs no special notation for this at all: a single directive is already one goal, and the comma that means "and" everywhere else in the language sequences side-effecting goals like writeln/1 exactly the same way.
Formatted output
import Text.Printf (printf) main :: IO () main = printf "%s scored %d points\n" "Alice" (42 :: Int)
:- format("~w scored ~w points~n", ['Alice', 42]).
Haskell's Text.Printf.printf is a genuinely unusual function — it is variadic via typeclasses, accepting a different number and type of arguments depending on the format string, resolved entirely at compile time. Prolog's format/2 takes the same idea but resolves it at runtime: ~w writes any term, and the argument list is always an ordinary Prolog list, with no type-level trickery involved.
Higher-Order
`map` returns a list; `maplist/3` relates two
double :: Int -> Int double n = n * 2 main :: IO () main = print (map double [1, 2, 3])
double(N, Doubled) :- Doubled is N * 2. :- maplist(double, [1, 2, 3], Result), writeln(Result).
Haskell's map takes a function and 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 is unified in place rather than returned, and Tau Prolog has no lambda syntax, so the relation must first be given a name the way double/2 is here.
Filtering: `filter` versus a generate-and-test query
main :: IO () main = print (filter even [1, 2, 3, 4, 5, 6])
:- findall(X, (member(X, [1, 2, 3, 4, 5, 6]), 0 is X mod 2), Result), writeln(Result).
Haskell's filter is a single, dedicated higher-order function. Prolog has no equivalent built-in name for this shape; instead it composes the same generate-and-test idea from parts already seen elsewhere on this page — member/2 generates every candidate on backtracking, the comma checks the condition, and findall/3 collects every candidate that survives into an ordinary list.
Summing a list
main :: IO () main = print (sum [1, 2, 3, 4])
:- sum_list([1, 2, 3, 4], Result), writeln(Result).
Haskell's sum is built on the general Foldable abstraction, so it also works on trees, Maybe, and any other foldable structure, not just lists. Prolog's sum_list/2 is specific to lists and, like the other list built-ins on this page, takes an explicit output argument rather than returning a value.
Collecting Solutions
`findall/3` is where "many answers" becomes visible
parents :: [(String, String)] parents = [("tom", "bob"), ("tom", "liz"), ("bob", "ann")] main :: IO () main = print [child | ("tom", child) <- parents]
parent(tom, bob). parent(tom, liz). parent(bob, ann). :- findall(Child, parent(tom, Child), Children), writeln(Children).
This is the concept that most separates the two languages. A Haskell list comprehension filters an already fully built list — the data exists in memory before the comprehension ever runs. findall(Child, parent(tom, Child), Children) instead runs parent(tom, Child) as a search, exhausting every possible way to prove it via backtracking, and only then collects the results into the list Children — the list did not exist until the search that produced it finished.
`setof/3` versus `nub` and `sort`
import Data.List (nub, sort) main :: IO () main = print (sort (nub [3, 1, 3, 2, 1]))
:- setof(X, member(X, [3, 1, 3, 2, 1]), Result), writeln(Result).
Getting a sorted list of unique values takes two separate function calls in Haskell — nub to deduplicate, then sort to order. Prolog's setof/3 does both in one built-in: it collects every solution like findall/3, but also removes duplicates and sorts the result into Prolog's standard order of terms automatically. (Unlike findall/3, setof/3 fails outright rather than returning an empty list when there are no solutions at all.)
Mutable State vs. the Dynamic Database
A genuinely mutable database, unlike anything pure Haskell offers
-- Pure Haskell has no way to add a new top-level fact while the program runs; -- you would need IORef or the State monad, and even those require an -- explicit, type-tracked mutable context rather than a bare assignment. main :: IO () main = let discoveries = ["alice"] ++ ["bob"] in mapM_ putStrLn discoveries
:- dynamic(person/1). person(alice). :- assertz(person(bob)), forall(person(Who), writeln(Who)).
This is the place Prolog most sharply breaks from the values a Haskeller has internalized. assertz/1 adds a brand-new fact to the running program's own database while it executes — genuinely mutable global state with no type tracking it at all. A predicate must first be declared :- dynamic(person/1)., since Prolog otherwise treats its clause database as fixed once loaded. Pure Haskell has no equivalent whatsoever; the closest analogues, IORef or the State monad, both require an explicit, statically-visible mutable context — Haskell keeps you honest about exactly where mutation can occur, and Prolog's database offers no such boundary.
`retract/1` takes a fact back out
-- Again, pure Haskell has no destructive removal from a top-level definition; -- the closest equivalent is simply building a new, filtered list. main :: IO () main = let remaining = filter (/= "bob") ["alice", "bob"] in mapM_ putStrLn remaining
:- dynamic(person/1). person(alice). person(bob). :- retract(person(bob)), forall(person(Who), writeln(Who)).
retract/1 is the destructive counterpart to assertz/1: it removes a matching fact from the database outright. There is no Haskell equivalent to reach for at all — Haskell values are immutable by construction, so "removing" something always means building a new value (here, via filter) rather than mutating an existing one in place.
Classic Examples
Quicksort — famously elegant in both languages
quicksort :: [Int] -> [Int] quicksort [] = [] quicksort (x : xs) = quicksort [a | a <- xs, a < x] ++ [x] ++ quicksort [a | a <- xs, a >= x] main :: IO () main = print (quicksort [3, 1, 4, 1, 5, 9, 2, 6])
quicksort([], []). quicksort([Pivot | Rest], Sorted) :- partition_by(Pivot, Rest, Smaller, GreaterOrEqual), quicksort(Smaller, SortedSmaller), quicksort(GreaterOrEqual, SortedGreaterOrEqual), append(SortedSmaller, [Pivot | SortedGreaterOrEqual], Sorted). partition_by(_, [], [], []). partition_by(Pivot, [Head | Tail], [Head | Smaller], GreaterOrEqual) :- Head < Pivot, !, partition_by(Pivot, Tail, Smaller, GreaterOrEqual). partition_by(Pivot, [Head | Tail], Smaller, [Head | GreaterOrEqual]) :- partition_by(Pivot, Tail, Smaller, GreaterOrEqual). :- quicksort([3, 1, 4, 1, 5, 9, 2, 6], Sorted), writeln(Sorted).
Quicksort is the single most-cited "beautiful in this paradigm" example for both languages, but the beauty comes from different sources. Haskell's version is beautiful because list comprehensions let the partition step read as a direct transcription of the mathematical definition. Prolog's version needs an explicit helper predicate to build the two partitions as relations, but its own beauty is that quicksort/2, unification, and append/3's reversibility together mean the same predicate can, in principle, also be queried in reverse.
Fibonacci by pattern-matched equations
fibonacci :: Int -> Integer fibonacci 0 = 0 fibonacci 1 = 1 fibonacci n = fibonacci (n - 1) + fibonacci (n - 2) main :: IO () main = print (map fibonacci [0 .. 10])
fibonacci(0, 0). fibonacci(1, 1). fibonacci(N, Result) :- N > 1, N1 is N - 1, N2 is N - 2, fibonacci(N1, Fib1), fibonacci(N2, Fib2), Result is Fib1 + Fib2. :- findall(Result, (between(0, 10, N), fibonacci(N, Result)), Results), writeln(Results).
The recursive structure is identical in shape — two base cases, one recursive case that adds two smaller calls together — and neither version memoizes, so both recompute the same subproblems exponentially many times. Producing the first eleven values needs map in Haskell and between/3 plus findall/3 in Prolog, mirroring the "eager list versus generate-then-collect" contrast seen throughout this page.
The classic family-tree example
parents :: [(String, String)] parents = [("tom", "bob"), ("tom", "liz"), ("bob", "ann"), ("bob", "pat")] main :: IO () main = print [child | (parentName, child) <- parents, parentName == "bob"]
parent(tom, bob). parent(tom, liz). parent(bob, ann). parent(bob, pat). grandparent(GrandparentName, GrandchildName) :- parent(GrandparentName, ChildName), parent(ChildName, GrandchildName). :- findall(Grandchild, grandparent(tom, Grandchild), Grandchildren), writeln(Grandchildren).
This is Prolog's single most iconic teaching example, and it is worth seeing exactly why it resists a clean Haskell translation: grandparent(GrandparentName, GrandchildName) :- parent(GrandparentName, ChildName), parent(ChildName, GrandchildName). reads almost like an English sentence and works in any direction — ask for Tom's grandchildren, or ask whose grandparent Ann is, with the same two-line rule. A Haskell version has to commit up front to one specific direction of query, spelled out as an explicit list comprehension over a list you already built by hand.