Intermediate

Functions in Prolog

Learn how Prolog uses predicates as functions - input/output arguments, recursion, multiple solutions, and higher-order predicates with Docker-ready examples

Most languages have functions that take inputs and return an output. Prolog has no functions in that sense. Instead, the unit of reusable logic is the predicate - a named relation between terms. A predicate doesn’t “return” a value the way a function does; it succeeds or fails, and any results it computes are handed back through its arguments.

As a logic programming language, Prolog blurs the line between input and output. The predicate double(X, Y) describes a relationship - “Y is twice X” - and depending on which arguments you supply, Prolog can run it in different directions. There is no return keyword, no call stack you manage by hand, and no distinction between a function and a procedure. A predicate either proves its goal or it doesn’t.

In this tutorial you’ll learn how to define predicates that act like functions, how to pass results back through arguments, how recursion replaces loops, how a single predicate can produce many answers through backtracking, and how higher-order predicates let you pass logic as data. Understanding predicates is the key to thinking declaratively.

Predicates as Functions

A predicate is defined by one or more clauses. To make a predicate behave like a function, you reserve one argument for the result and compute it with is/2, which evaluates arithmetic. By convention the output argument comes last.

Create a file named functions.pl:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
% A predicate "returns" a result through its last argument
double(X, Result) :- Result is X * 2.

square(X, Result) :- Result is X * X.

% A predicate can hand back several results at once -
% one output argument per value it computes
circle_props(Radius, Area, Circumference) :-
    Area is pi * Radius * Radius,
    Circumference is 2 * pi * Radius.

:- initialization(main).

main :-
    double(21, D),
    format('double(21) = ~w~n', [D]),
    square(9, S),
    format('square(9) = ~w~n', [S]),
    circle_props(5, A, C),
    format('circle area = ~2f, circumference = ~2f~n', [A, C]),
    halt.

The head of each clause (double(X, Result)) names the predicate and its arguments. The body after :- is the goal that must succeed. When you call double(21, D), Prolog unifies X with 21, evaluates 21 * 2, and binds D to 42. Because Prolog has no return statement, calling a predicate means querying it and reading the bound variables afterward.

Notice circle_props/3 produces two results. There is nothing special about returning multiple values in Prolog - you simply add more output arguments. The notation circle_props/3 means “the predicate named circle_props with 3 arguments”; arity is part of a predicate’s identity.

Recursion Instead of Loops

Prolog has no for or while loops. Repetition is expressed through recursion: a predicate that refers to itself, with one or more base cases to stop the recursion. This is the natural way to process numbers and lists.

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
% Base case: the factorial of 0 is 1
factorial(0, 1).
% Recursive case: N! = N * (N-1)!
factorial(N, F) :-
    N > 0,
    N1 is N - 1,
    factorial(N1, F1),
    F is N * F1.

% Recursively count the elements of a list
list_length([], 0).
list_length([_|T], N) :-
    list_length(T, N0),
    N is N0 + 1.

% Recursively add up the elements of a list
sum_elements([], 0).
sum_elements([H|T], Sum) :-
    sum_elements(T, Sum0),
    Sum is H + Sum0.

:- initialization(main).

main :-
    factorial(5, F),
    format('factorial(5) = ~w~n', [F]),
    list_length([a,b,c,d], Len),
    format('length = ~w~n', [Len]),
    sum_elements([10,20,30], Sum),
    format('sum = ~w~n', [Sum]),
    halt.

Each predicate has two clauses. The first is the base case - the simplest input where the answer is known directly. The second is the recursive case, which solves a smaller version of the problem and combines it with the current element. The list pattern [H|T] splits a list into its head H and tail T; recursing on T walks the list one element at a time. The guard N > 0 in factorial prevents the recursive clause from firing on 0, so only the base case matches there.

Multiple Solutions Through Backtracking

A function returns exactly one value. A Prolog predicate can succeed many times, offering a different answer each time you ask for more. This is backtracking, and it means a predicate can act like a function that returns a whole set of results. The findall/3 predicate collects every solution into a list.

Create a file named multiple_solutions.pl:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
% Three facts - color/1 has three solutions
color(red).
color(green).
color(blue).

% A small family knowledge base
parent(tom, bob).
parent(tom, liz).
parent(bob, ann).

% Gather every Child for which parent(Parent, Child) holds
all_children(Parent, Children) :-
    findall(Child, parent(Parent, Child), Children).

:- initialization(main).

main :-
    % Collect all solutions of color(C) into a list
    findall(C, color(C), Colors),
    format('colors = ~w~n', [Colors]),
    all_children(tom, Kids),
    format('tom''s children = ~w~n', [Kids]),
    halt.

color(C) on its own has three solutions: C = red, C = green, and C = blue. In the interactive REPL you’d press ; to step through them. findall(C, color(C), Colors) automates that, running the goal to exhaustion and binding Colors to [red, green, blue]. This is a defining feature of Prolog: a single predicate naturally models a one-to-many relationship, something a conventional function cannot do without returning a collection explicitly.

Higher-Order Predicates

Predicates are terms, so you can pass them to other predicates as data. The built-in call/N invokes a goal with extra arguments appended, and library predicates like maplist/3 and foldl/4 apply a predicate across a list. These are Prolog’s higher-order tools.

Create a file named higher_order.pl:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
% An ordinary two-argument predicate we can pass around
increment(X, Y) :- Y is X + 1.

% Take a predicate Goal and apply it to X twice
apply_twice(Goal, X, Result) :-
    call(Goal, X, Mid),
    call(Goal, Mid, Result).

:- initialization(main).

main :-
    % maplist applies increment to every element of the list
    maplist(increment, [1,2,3], Incremented),
    format('incremented = ~w~n', [Incremented]),
    % foldl accumulates a result; the lambda adds each element to the total
    foldl([X,A,B]>>(B is A + X), [1,2,3,4], 0, Total),
    format('total = ~w~n', [Total]),
    % call/3 invokes increment with the supplied arguments
    apply_twice(increment, 10, R),
    format('apply_twice(increment, 10) = ~w~n', [R]),
    halt.

call(Goal, X, Mid) takes the partial goal increment and calls it as increment(X, Mid), appending the two extra arguments. maplist(increment, [1,2,3], Incremented) applies increment element-wise, producing [2,3,4]. The foldl line uses a yall lambda - [X,A,B]>>(B is A + X) - an anonymous predicate written inline, where X is the list element, A the running accumulator, and B the next accumulator. Passing logic as an argument like this lets you write generic, reusable control patterns.

Variable Scope

Prolog’s scoping rule is simple: every variable is local to the single clause it appears in. The X in one clause has no connection to an X in another - there are no global variables and no shared mutable state. A variable is also single-assignment: once unified with a value within a clause, it keeps that value for the rest of that clause’s execution. Within a clause, the same name always refers to the same logical variable, which is how the inputs and outputs of a goal get connected. To share data between predicates, you pass it explicitly through arguments, exactly as the examples above do.

Running with Docker

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

# Run each example
docker run --rm -v $(pwd):/app -w /app swipl:stable swipl -q functions.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 multiple_solutions.pl
docker run --rm -v $(pwd):/app -w /app swipl:stable swipl -q higher_order.pl

The -q flag suppresses the startup banner. The initialization(main) directive in each file tells SWI-Prolog to run main automatically once the file has loaded.

Expected Output

functions.pl:

double(21) = 42
square(9) = 81
circle area = 78.54, circumference = 31.42

recursion.pl:

factorial(5) = 120
length = 4
sum = 60

multiple_solutions.pl:

colors = [red,green,blue]
tom's children = [bob,liz]

higher_order.pl:

incremented = [2,3,4]
total = 10
apply_twice(increment, 10) = 12

Key Concepts

  • Predicates replace functions - a predicate is a named relation that succeeds or fails; it doesn’t return a value, it binds output arguments.
  • Results flow through arguments - reserve one or more arguments (by convention, the last) for outputs and compute them with is/2. Multiple outputs just means multiple arguments.
  • Arity matters - foo/2 and foo/3 are different predicates; the name plus argument count identifies it.
  • Recursion replaces loops - define a base case and a recursive case; the [H|T] pattern walks lists one element at a time.
  • One predicate, many answers - backtracking lets a predicate succeed repeatedly; findall/3 collects every solution into a list.
  • Predicates are first-class - pass them as data with call/N, maplist/3, and foldl/4, and write inline logic with yall lambdas ([Args]>>Goal).
  • Variables are clause-local and single-assignment - there is no global state; share data only by passing it through arguments.

Running Today

All examples can be run using Docker:

docker pull swipl:stable
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining