Next: AssertRetract, Previous: Syntax, Up: Top [Contents]
Mercury is a purely declarative language.
Therefore it cannot use Prolog’s mechanism for doing
input and output with side-effects.
The mechanism that Mercury uses is the threading of an object
that represents the state of the world through the computation.
The type of this object is io.state
, or just io
for short.
Each operation that affects the state of the world
must have two arguments of this type,
representing respectively the state of the world before the operation,
and the state of the world after the operation.
The modes of the two arguments that are added to calls are
di
for “destructive input” and uo
for “unique output”.
The first means that the input variable
must be the last reference to the original state of the world,
and the latter means that the output variable is guaranteed to be the
only reference to the state of the world produced by this predicate.
For example, the direct translation of the Prolog predicate
write_total(Total) :- write('The total is '), write(Total), write('.'), nl.
into Mercury yields this Mercury predicate:
:- pred write_total(int::in, io::di, io::uo) is det. write_total(Total, IO0, IO) :- print("The total is ", IO0, IO1), print(Total, IO1, IO2), print('.', IO2, IO3), nl(IO3, IO).
The variables IO0
, IO1
etc each represent
one version of the state of the world.
IO0
represents the state before the total is printed,
IO1
represents the state after just The total is
is printed,
and so on.
However, programmers usually don’t want to give specific names
to all these different versions;
they want to name only the entities
that all these variables represent different versions of.
That is why Mercury supports state variable notation.
This is syntactic sugar
designed to make it easier to thread a sequence of variables
holding the successive states of an entity through a clause.
You as the programmer name only the entity,
and let the compiler name the various versions.
With state variables, the above clause would be written as
write_total(Total, !IO) :- print("The total is ", !IO), print(Total, !IO), print('.', !IO), nl(!IO).
and the compiler will internally convert this clause
into code that looks like the previous clause.
(The usual convention in Mercury programs
is to name the state variable representing the state of the world !IO
.)
In the head of a clause, what looks like an argument that consists of a variable name prefixed by an exclamation mark actually stands for two arguments which are both variables, holding the initial and final state of whatever entity the state variable stands for. In this case, they stand for the state of the world, respectively before and after the line about the total has been printed. In calls in the body of a clause, what looks like an argument that consists of a variable name prefixed by an exclamation mark also stands for two arguments which are both variables, but these hold respectively, the current and the next state.
In Prolog, it is quite normal to give to print
an argument that is an atom that is not used anywhere else in the program,
or at least not in code related to the code that does the printing.
This is because the term being printed
does not have to belong to a defined type.
Since Mercury is strongly typed, the atom being printed
would have to be a data constructor of a defined type.
A Mercury programmer could define a meaningless type
just to give one of its data constructors to a call to print
,
but it is far better to simply call a predicate specifically designed
to print the string, or integer, or character, you want to print:
write_total(Total, !IO) :- io.write_string("The total is ", !IO), io.write_int(Total, !IO), io.write_char('.', !IO), io.nl(!IO).
The io.
prefix on the predicates called in the body indicates that
the callees are in the io
module of the Mercury standard library.
This module contains all of Mercury’s primitive I/O operations.
These module qualifications are not strictly necessary
(unless two or more modules define predicates
with the same names and argument types,
the Mercury compiler can figure out which modules called predicates are in),
but Mercury convention is to make the module qualifier explicit
in order to make the intent of the code crystal clear to readers.
The above could also be written more compactly like this:
write_total(Total, !IO) :- io.format("The total is %d.\n", [i(Total)], !IO).
The first argument of io.format
is a format string
modelled directly on the format strings supported by printf
in C,
while the second is a list of the values to be printed,
which should have one value for each conversion specifier.
In this case, there is one conversion specifier, ‘%d’,
which calls for the printing of an integer as a decimal number,
and the corresponding value is the integer Total
.
Since Mercury is strongly typed,
and different arguments may have different types,
in the argument list
integers must be wrapped inside i()
,
floats must be wrapped inside f()
,
strings must be wrapped inside s()
, and
chars must be wrapped inside c()
.
Despite appearances, in the usual case of the format string being constant,
the wrappers and the list of arguments have neither time nor space overhead,
because the compiler optimizes them away,
replacing the call to io.format
with
the calls to io.write_string
, io.write_int
etc above.
One of the important consequences of our model for input and output is that predicates that can fail may not do input or output. This is because the state of the world must be a unique object, and each I/O operation destructively replaces it with a new state. Since each I/O operation destroys the current state object and produces a new one, it is not possible for I/O to be performed in a context that may fail, since when failure occurs the old state of the world will have been destroyed, and since bindings cannot be exported from a failing computation, the new state of the world is not accessible.
In some circumstances, Prolog programs that suffer from this problem can be fixed by moving the I/O out of the failing context. For example
… ( solve(Goal) -> … ; … ), ...
where ‘solve(Goal)’ does some I/O can be transformed into valid Mercury in at least two ways. The first is to make ‘solve’ deterministic and return a status:
… solve(Goal, Result, !IO), ( Result = success(…), … ; Result = failure, … ), …
The other way is to transform ‘solve’ so that all the input and output takes place outside it:
... io.write_string("calling: ", !IO), solve.write_goal(Goal, !IO), ( solve(Goal) -> io.write_string("succeeded\n", !IO), ... ; ... ), ...
Next: AssertRetract, Previous: Syntax, Up: Top [Contents]