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:
| |
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:
| |
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:
| |
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:
| |
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
| |
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/2andfoo/3are 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/3collects every solution into a list. - Predicates are first-class - pass them as data with
call/N,maplist/3, andfoldl/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.
Comments
Loading comments...
Leave a Comment