Intermediate

I/O Operations in Prolog

Learn input and output in Prolog - console output, reading from stdin, file streams, and reading and writing terms with Docker-ready examples

Input and output occupy an unusual place in Prolog. The language is fundamentally declarative — you describe relationships and let the resolution engine search for solutions. But I/O is inherently about side effects: printing to a terminal, consuming a line of input, writing bytes to a file. These actions happen once, in order, and can’t be backtracked away.

Prolog handles this tension by treating I/O predicates as goals that always succeed (once) while producing their effect as a side effect of proof. When you write write(hello), nl, Prolog proves write(hello) — printing hello along the way — then proves nl, printing a newline. Because these goals are deterministic, backtracking into them does nothing useful, so they behave much like statements in an imperative language.

What makes Prolog’s I/O distinctive is that its natural unit is the term, not the string. Alongside character- and line-oriented predicates, Prolog offers read/1 and write/1, which serialize and parse full Prolog terms — compound structures, lists, numbers, and atoms — using the same syntax you write in source files. This makes a Prolog program’s data format and its code format one and the same.

This tutorial covers console output, formatted output with format/2, reading from standard input, working with file streams, and the term-oriented I/O that is unique to logic programming. All examples run on SWI-Prolog via Docker.

Console Output Predicates

Prolog provides several predicates for writing to the current output stream. Each treats quoting and spacing differently.

Create a file named output.pl:

1
2
3
4
5
6
7
8
:- initialization(main).

main :-
    write('Plain write: '), write('hello world'), nl,
    write('Quoted writeq: '), writeq('hello world'), nl,
    writeln('writeln adds a newline for you'),
    write('Indented:'), tab(4), writeln('after 4 spaces'),
    halt.
  • write/1 prints a term without quoting. The atom 'hello world' prints as hello world, dropping the quotes.
  • writeq/1 prints a term so it can be read back by read/1. Because hello world contains a space, it prints with quotes: 'hello world'.
  • writeln/1 is a convenience predicate that writes a term followed by a newline.
  • nl/0 writes a single newline; tab/1 writes N spaces.

Formatted Output with format/2

For anything beyond simple values, format/2 is the workhorse. It takes a format string with ~-directives and a list of arguments, similar in spirit to C’s printf but with directives suited to Prolog.

Create a file named formatted.pl:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
:- initialization(main).

main :-
    format('Name: ~w, Age: ~d~n', [alice, 30]),
    format('Pi to 4 places: ~4f~n', [3.14159265]),
    format('Hex of 255: ~16r~n', [255]),
    format('Char from code 65: ~c~n', [65]),
    format('~d items in stock~n', [7]),
    print_table,
    halt.

print_table :-
    format('~w~t~10|~w~n', ['Item', 'Qty']),
    format('~w~t~10|~w~n', ['apples', 12]),
    format('~w~t~10|~w~n', ['pears', 5]).

The directives used here:

  • ~w writes an argument (like write/1).
  • ~d prints an integer; ~Nf prints a float with N decimals (~4f → four places).
  • ~Nr prints an integer in radix N (~16r → hexadecimal).
  • ~c prints the character for a character code (65A).
  • ~t and ~N| together create column alignment: ~t inserts fill (spaces by default) and ~10| sets a column stop at position 10, so each label is padded out to a fixed width.

Reading Input from Standard Input

Prolog reads user input from the user_input stream. read_line_to_string/2 reads one line as a string, which you can then convert to a number or other term.

Create a file named input.pl:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
:- initialization(main).

main :-
    write('Enter your name: '),
    read_line_to_string(user_input, Name),
    format('Hello, ~w!~n', [Name]),
    write('Enter a number: '),
    read_line_to_string(user_input, NumStr),
    number_string(Num, NumStr),
    Doubled is Num * 2,
    format('~w doubled is ~w~n', [Num, Doubled]),
    halt.

Here read_line_to_string/2 binds Name to the text of the first input line (as a string, without the trailing newline). number_string/2 parses the second line into an integer, and is/2 evaluates the arithmetic. Because the prompts are printed with write/1 and no newline, they appear directly before each result when input is piped in.

Writing and Reading Files (Line-Oriented)

File I/O in Prolog goes through streams. You open/3 a file to get a stream handle, read or write through it, then close/1 it. The idiomatic way to guarantee the stream is closed — even if a goal fails or throws — is setup_call_cleanup/3.

Create a file named file_io.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
32
33
:- initialization(main).

main :-
    write_data,
    read_data,
    halt.

write_data :-
    setup_call_cleanup(
        open('languages.txt', write, Out),
        (   format(Out, '~w ~d~n', [prolog, 1972]),
            format(Out, '~w ~d~n', [erlang, 1986]),
            format(Out, '~w ~d~n', [python, 1991])
        ),
        close(Out)
    ),
    writeln('Wrote 3 lines to languages.txt').

read_data :-
    writeln('Reading languages.txt back:'),
    setup_call_cleanup(
        open('languages.txt', read, In),
        read_lines(In),
        close(In)
    ).

read_lines(In) :-
    read_line_to_string(In, Line),
    (   Line == end_of_file
    ->  true
    ;   format('  ~w~n', [Line]),
        read_lines(In)
    ).

Notice that format/3 (and write/2, nl/1, etc.) take an explicit stream as the first argument, directing output to the file instead of the console. Reading loops via recursion — Prolog’s substitute for a while loop — until read_line_to_string/2 returns the atom end_of_file. The ( Cond -> Then ; Else ) construct is Prolog’s if-then-else.

Reading and Writing Terms

The most Prolog-native form of I/O reads and writes terms rather than raw text. writeq/2 writes a term in a form that can be parsed back, and read/2 reads a term terminated by a period. This lets a file double as a serialized data structure.

Create a file named terms_io.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
32
33
:- initialization(main).

main :-
    write_terms,
    read_terms,
    halt.

write_terms :-
    setup_call_cleanup(
        open('facts.pl', write, Out),
        (   writeq(Out, point(1, 2)), write(Out, '.'), nl(Out),
            writeq(Out, point(3, 4)), write(Out, '.'), nl(Out)
        ),
        close(Out)
    ).

read_terms :-
    setup_call_cleanup(
        open('facts.pl', read, In),
        read_all_terms(In),
        close(In)
    ).

read_all_terms(In) :-
    read(In, Term),
    (   Term == end_of_file
    ->  true
    ;   format('Read term: ~w~n', [Term]),
        Term = point(X, Y),
        Sum is X + Y,
        format('  Sum of coords: ~w~n', [Sum]),
        read_all_terms(In)
    ).

Each line written to facts.pl is a valid Prolog term followed by a period — exactly the syntax Prolog uses in source files. When reading, read/2 parses each term back into a structured point(X, Y), which unifies cleanly so we can pull out the coordinates and compute with them. At end of file, read/2 yields end_of_file. This term round-tripping is what makes Prolog’s data and code representations identical.

Running with Docker

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Pull the official SWI-Prolog image
docker pull swipl:stable

# Console output predicates
docker run --rm -v $(pwd):/app -w /app swipl:stable swipl -q output.pl

# Formatted output
docker run --rm -v $(pwd):/app -w /app swipl:stable swipl -q formatted.pl

# Reading from stdin (-i keeps stdin open; input is piped in)
printf 'Ada\n21\n' | docker run --rm -i -v $(pwd):/app -w /app swipl:stable swipl -q input.pl

# File I/O (writes and reads languages.txt in the mounted directory)
docker run --rm -v $(pwd):/app -w /app swipl:stable swipl -q file_io.pl

# Term I/O (writes and reads facts.pl in the mounted directory)
docker run --rm -v $(pwd):/app -w /app swipl:stable swipl -q terms_io.pl

The -q flag suppresses the SWI-Prolog banner. Because the current directory is mounted at /app, files written by the programs (languages.txt, facts.pl) appear in your local folder.

Expected Output

Running output.pl:

Plain write: hello world
Quoted writeq: 'hello world'
writeln adds a newline for you
Indented:    after 4 spaces

Running formatted.pl:

Name: alice, Age: 30
Pi to 4 places: 3.1416
Hex of 255: ff
Char from code 65: A
7 items in stock
Item      Qty
apples    12
pears     5

Running input.pl with the piped input Ada then 21:

Enter your name: Hello, Ada!
Enter a number: 21 doubled is 42

Running file_io.pl:

Wrote 3 lines to languages.txt
Reading languages.txt back:
  prolog 1972
  erlang 1986
  python 1991

Running terms_io.pl:

Read term: point(1,2)
  Sum of coords: 3
Read term: point(3,4)
  Sum of coords: 7

Key Concepts

  • I/O predicates are deterministic side effects. They succeed once and produce their effect during proof; backtracking cannot undo output that has already happened.
  • write/1 vs writeq/1. Use write/1 for human-readable output and writeq/1 (or writeq/2) when the output must be parseable back into a term by read/1.
  • format/2 is the flexible workhorse. Directives like ~w, ~d, ~Nf, ~Nr, and ~c handle common formatting, while ~t and ~N| provide column alignment.
  • Streams model files. open/3 returns a stream handle; predicates like write/2 and format/3 take that stream as an explicit first argument to redirect output.
  • setup_call_cleanup/3 guarantees cleanup. It ensures close/1 runs whether the body succeeds, fails, or throws — the safe way to manage file handles.
  • Recursion replaces loops. Reading a whole file is expressed as a recursive predicate that stops when the read predicate returns end_of_file.
  • Term I/O is uniquely Prolog. read/1 and write/1 serialize and parse full terms using Prolog’s own syntax, so files and source code share one representation.
  • Convert input explicitly. Line reads produce strings; use number_string/2 (or atom_number/2, term_string/2) to turn text into numbers or terms before computing.

Running Today

All examples can be run using Docker:

docker pull swipl:stable
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining