Beginner

Control Flow in Prolog

Learn how control flow works in Prolog through backtracking, if-then-else, cut, negation as failure, and recursion with Docker-ready examples

Control flow in Prolog looks nothing like the if/for/while machinery of imperative languages — and that is the whole point. Prolog is a logic programming language, so the engine’s default behavior is control flow. When you pose a goal, Prolog searches its knowledge base for a way to prove it, and when a choice fails it backtracks to try the next alternative. You don’t write the loop; the search strategy is the loop.

That said, Prolog gives you explicit tools to shape the search when you need them: the ( Cond -> Then ; Else ) if-then-else construct, the cut (!) to commit to a decision and prune the search, and negation as failure (\+) to ask “can this not be proven?”. Repetition that other languages express with loops is expressed in Prolog with recursion, or with all-solutions predicates like forall/2, findall/3, and between/3.

This tutorial works through each of these. By the end you’ll understand how to make decisions, iterate over ranges and solutions, commit to choices with the cut, and express loops as recursive predicates — all while thinking in terms of proof and search rather than step-by-step instructions.

Conditionals: Multiple Clauses and If-Then-Else

There are two idiomatic ways to make a decision in Prolog. The first is the most declarative: write several clauses, each with its own condition, and let unification and backtracking pick the one that applies. The second is the explicit ( Condition -> Then ; Else ) operator, which reads much like a traditional if/else chain.

Create a file named if_then_else.pl:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
% if_then_else.pl - Conditionals in Prolog

% The declarative way: multiple clauses, each guarded by a condition
sign(N, positive) :- N > 0.
sign(N, negative) :- N < 0.
sign(0, zero).

% The if-then-else operator: ( Cond -> Then ; Else )
% Chains read top to bottom; the first true condition wins.
grade(Score, Letter) :-
    ( Score >= 90 -> Letter = 'A'
    ; Score >= 80 -> Letter = 'B'
    ; Score >= 70 -> Letter = 'C'
    ; Letter = 'F'
    ).

:- sign(7, S1),  format('7 is ~w~n', [S1]),
   sign(-2, S2), format('-2 is ~w~n', [S2]),
   sign(0, S3),  format('0 is ~w~n', [S3]),
   grade(95, G1), format('Score 95: ~w~n', [G1]),
   grade(83, G2), format('Score 83: ~w~n', [G2]),
   grade(71, G3), format('Score 71: ~w~n', [G3]),
   grade(50, G4), format('Score 50: ~w~n', [G4]),
   halt.

The sign/2 predicate shows the declarative style: each clause is a separate fact-like rule, and Prolog tries them in order. The grade/2 predicate uses the if-then-else operator, where -> means “if the condition on the left succeeds, commit to the goal on the right.” Only the first matching branch runs — the -> quietly cuts away the alternatives.

Backtracking: The Loop You Didn’t Write

Backtracking is Prolog’s native control flow. A predicate with multiple matching clauses is a generator of solutions, and the engine will walk through every one of them on demand. We can harvest those solutions with findall/3 (collect them all into a list) or iterate over them with forall/2 (a side-effecting “for every solution, do this”).

Create a file named backtracking.pl:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
% backtracking.pl - Backtracking IS control flow

color(red).
color(green).
color(blue).

dish(soup).
dish(salad).

% Every combination falls out of backtracking automatically
meal(Color, Dish) :- color(Color), dish(Dish).

:- writeln('All colors (via forall + backtracking):'),
   forall(color(C), (write('  '), writeln(C))),
   writeln('All meal combinations:'),
   findall(Color-Dish, meal(Color, Dish), Meals),
   forall(member(M, Meals), (write('  '), writeln(M))),
   length(Meals, N),
   format('Total combinations: ~w~n', [N]),
   halt.

Notice there is no nested loop here. The goal meal(Color, Dish) calls color(Color) and dish(Dish); Prolog automatically tries every color paired with every dish, backtracking through all six combinations. findall/3 gathers them into the list Meals, and forall/2 prints each one. This is how Prolog expresses “for each X, for each Y” without any explicit iteration construct.

Recursion: Loops Without Loops

Prolog has no for or while. Repetition is expressed with recursion: a predicate that calls itself with a smaller problem until it reaches a base case. This is the single most important control-flow pattern in the language.

Create a file named recursion.pl:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
% recursion.pl - Recursion replaces loops

% Countdown: a "loop" expressed as recursion
countdown(0) :- writeln('Liftoff!').
countdown(N) :-
    N > 0,
    writeln(N),
    N1 is N - 1,
    countdown(N1).

% Factorial: accumulate a result through the recursive calls
factorial(0, 1).
factorial(N, F) :-
    N > 0,
    N1 is N - 1,
    factorial(N1, F1),
    F is N * F1.

% Sum a list by walking its head and tail
sum_list_recursive([], 0).
sum_list_recursive([H|T], Sum) :-
    sum_list_recursive(T, Rest),
    Sum is H + Rest.

:- writeln('Countdown from 3:'),
   countdown(3),
   factorial(5, F),
   format('5! = ~w~n', [F]),
   sum_list_recursive([10, 20, 30, 40], S),
   format('Sum of [10,20,30,40] = ~w~n', [S]),
   halt.

Each predicate has a base case (the terminating condition, like countdown(0) or factorial(0, 1)) and a recursive case that reduces the problem toward that base. The N > 0 guard in countdown/2 and factorial/2 is essential — it stops the recursive clause from matching once we’ve reached zero, which is how recursion stands in for a loop’s exit condition.

Cut and Negation as Failure

Sometimes you want to prune Prolog’s search. The cut (!) tells the engine “I’m committed to the choices I’ve made so far — don’t backtrack past this point.” It’s how you make a predicate deterministic. The complementary tool is negation as failure (\+ Goal), which succeeds precisely when Prolog cannot prove Goal.

Create a file named cut_negation.pl:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
% cut_negation.pl - Cut and negation as failure

% Cut (!) commits to the first matching clause, preventing backtracking
max(X, Y, X) :- X >= Y, !.
max(_, Y, Y).

% A small knowledge base
likes(alice, prolog).
likes(alice, logic).
likes(bob, python).

% Negation as failure: \+ Goal succeeds if Goal cannot be proven
dislikes(Person, Thing) :- \+ likes(Person, Thing).

:- max(7, 3, M1), format('max(7,3) = ~w~n', [M1]),
   max(2, 9, M2), format('max(2,9) = ~w~n', [M2]),
   ( likes(alice, prolog)    -> writeln('alice likes prolog')          ; true ),
   ( dislikes(bob, prolog)   -> writeln('bob does not like prolog')    ; true ),
   ( dislikes(alice, prolog) -> writeln('alice does not like prolog')
                              ;  writeln('alice DOES like prolog') ),
   halt.

In max/3, once X >= Y succeeds the cut commits to the first clause — without it, Prolog could backtrack into the second clause and produce a wrong second answer. With dislikes/2, \+ likes(bob, prolog) succeeds because there is no fact saying Bob likes Prolog, while \+ likes(alice, prolog) fails because that fact does exist. This is “closed-world” reasoning: anything not provable is treated as false.

Running with Docker

Run each example with the official SWI-Prolog image. The -q flag suppresses the startup banner for clean output.

1
2
3
4
5
6
7
8
# Pull the official image
docker pull swipl:stable

# Run each control-flow example
docker run --rm -v $(pwd):/app -w /app swipl:stable swipl -q if_then_else.pl
docker run --rm -v $(pwd):/app -w /app swipl:stable swipl -q backtracking.pl
docker run --rm -v $(pwd):/app -w /app swipl:stable swipl -q recursion.pl
docker run --rm -v $(pwd):/app -w /app swipl:stable swipl -q cut_negation.pl

Expected Output

Running if_then_else.pl:

7 is positive
-2 is negative
0 is zero
Score 95: A
Score 83: B
Score 71: C
Score 50: F

Running backtracking.pl:

All colors (via forall + backtracking):
  red
  green
  blue
All meal combinations:
  red-soup
  red-salad
  green-soup
  green-salad
  blue-soup
  blue-salad
Total combinations: 6

Running recursion.pl:

Countdown from 3:
3
2
1
Liftoff!
5! = 120
Sum of [10,20,30,40] = 100

Running cut_negation.pl:

max(7,3) = 7
max(2,9) = 9
alice likes prolog
bob does not like prolog
alice DOES like prolog

Key Concepts

  • Backtracking is the default control flow — when a goal fails, Prolog automatically retreats to the last choice point and tries another alternative. Most “iteration” in Prolog is just the engine exploring solutions.
  • Multiple clauses are conditional branches — writing several guarded clauses for a predicate (like sign/2) is the declarative way to express alternatives, and it’s usually preferred over the if-then-else operator.
  • ( Cond -> Then ; Else ) is explicit if-then-else — the -> commits to the Then branch as soon as Cond succeeds, and you can chain conditions to build elif-style ladders.
  • Recursion replaces loops — every repetitive task uses a base case plus a recursive case that shrinks the problem; the guard (e.g. N > 0) acts as the loop’s exit condition.
  • findall/3, forall/2, and between/3 let you collect or iterate over all solutions and numeric ranges without manual looping.
  • The cut (!) prunes the search — it commits to choices already made, turning a non-deterministic predicate into a deterministic one. Use it deliberately, as it discards alternative solutions.
  • Negation as failure (\+) succeeds when a goal cannot be proven, reflecting Prolog’s closed-world assumption: unknown means false.

Running Today

All examples can be run using Docker:

docker pull swipl:stable
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining