2019-06-23
Getting Started with Mercury
Mercury is a logic programming language, similar to Prolog, but with static types. It feels like a combination of SML and Prolog at times. It was designed to help with programming large systems - that is large programs, large teams and better reliability, etc. The commercial product Prince XML is written in Mercury.
I've played around with Mercury in the past but haven't done anything substantial with it. Recently I picked it up again. This post is a short introduction to building Mercury, and some example "Hello World" style programs to test the install.
Build
Mercury is written in the Mercury language itself. This means it needs a Mercury compiler to bootstrap from. The way I got a build going from source was to download the source for a release of the day version, build that, then use that build to build the Mercury source from github. The steps are outlined in the README.bootstrap file, but the following commands are the basic steps:
$ wget http://dl.mercurylang.org/rotd/mercury-srcdist-rotd-2019-06-22.tar.gz
$ tar xvf mercury-srcdist-rotd-2019-06-22.tar.gz
$ cd mercury-srcdist-rotd-2019-06-22
$ ./configure --enable-minimal-install --prefix=/tmp/mercury
$ make
$ make install
$ cd ..
$ export PATH=/tmp/mercury/bin:$PATH
With this minimal compiler the main source can be built. Mercury has a number of backends, called 'grades' in the documentation. Each of these grades makes a number of tradeoffs in terms of generated code. They define the platform (C, assembler, Java, etc), whether GC is used, what type of threading model is available (if any), etc. The Adventures in Mercury blog has an article on some of the different grades. Building all of them can take a long time - multiple hours - so it pays to limit it if you don't need some of the backends.
For my purposes I didn't need the CSharp backend, but wanted to
explore the others. I was ok with the time tradeoff of building the
system. To build from the master
branch of the github repository I
did the following steps:
$ git clone https://github.com/Mercury-Language/mercury
$ cd mercury
$ ./prepare.sh
$ ./configure --enable-nogc-grades --disable-csharp-grade \
--prefix=/home/myuser/mercury
$ make PARALLEL=-j4
$ make install PARALLEL=-j4
$ export PATH=/home/myuser/mercury/bin:$PATH
Change the prefix
to where you want Mercury installed. Add the
relevant directories to the PATH as specified by the end of the build
process.
Hello World
A basic "Hello World" program in Mercury looks like the following:
:- module hello.
:- interface.
:- import_module io.
:- pred main(io, io).
:- mode main(di, uo) is det.
:- implementation.
main(IO0, IO1) :-
io.write_string("Hello World!\n", IO0, IO1).
With this code in a hello.m
file, it can be built and run with:
$ mmc --make hello
Making Mercury/int3s/hello.int3
Making Mercury/ints/hello.int
Making Mercury/cs/hello.c
Making Mercury/os/hello.o
Making hello
$ ./hello
Hello World!
The first line defines the name of the module:
:- module hello.
Following that is the definitions of the public interface of the module:
:- interface.
:- import_module io.
:- pred main(io, io).
:- mode main(di, uo) is det.
We publically import the io
module, as we use io
definitions in
the main
predicate. This is followed by a declaration of the
interface of main
- like C this is the user function called by the
runtime to execute the program. The definition here declares that
main
is a predicate, it takes two arguments, of type io
. This is a
special type that represents the "state of the world" and is how I/O
is handled in Mercury. The first argument is the "input world state"
and the second argument is the "output world state". All I/O functions
take these two arguments - the state of the world before the function
and the state of the world after.
The mode
line declares aspects of a predicate related to the logic
programming side of things. In this case we declare that the two
arguments passed to main
have the "destructive input" mode and the
"unique output" mode respectively. These modes operate similar to how
linear types work in other languages, and the reference manual has a
section describing
them. For
now the details can be ignored. The is det
portion identifies the
function as being deterministic. It always succeeds, doesn't backtrack
and only has one result.
The remaining code is the implementation. In this case it's just the implementation of the main
function:
main(IO0, IO1) :-
io.write_string("Hello World!\n", IO0, IO1).
The two arguments to main
, are the io
types representing the before and after representation of the world. We call write_string
to display a string, passing it the input world state, IO0
and receiving the new world state in IO1
. If we wanted to call an additional output function we'd need to thread these variables, passing the obtained output state as the input to the new function, and receiving a new output state. For example:
main(IO0, IO1) :-
io.write_string("Hello World!\n", IO0, IO1),
io.write_string("Hello Again!\n", IO1, IO2).
This state threading can be tedious, especially when refactoring - the need to renumber or rename variables is a pain point. Mercury has syntactic sugar for this called state variables, enabling this function to be written like this:
main(!IO) :-
io.write_string("Hello World!\n", !IO),
io.write_string("Hello Again!\n", !IO).
When the compiler sees !Variable_name
in an argument list it creates two arguments with automatically generated names as needed.
Another syntactic short cut can be done in the pred
and mode
lines. They can be combined into one line that looks like:
:- pred main(io::di, io::uo) is det.
Here the modes di
and uo
are appended to the type prefixed with a ::
. The resulting program looks like:
- module hello.
:- interface.
:- import_module io.
:- pred main(io::di, io::uo) is det.
:- implementation.
main(!IO) :-
io.write_string("Hello World!\n", !IO),
io.write_string("Hello Again!\n", !IO).
Factorial
The following is an implementation of factorial:
:- module fact.
:- interface.
:- import_module io.
:- pred main(io::di, io::uo) is det.
:- implementation.
:- import_module int.
:- pred fact(int::in, int::out) is det.
fact(N, X) :-
( N = 1 -> X = 1 ; fact(N - 1, X0), X = N * X0 ).
main(!IO) :-
fact(5, X),
io.print("fact(5, ", !IO),
io.print(X, !IO),
io.print(")\n", !IO).
In the implementation section here we import the int
module
to access functions across machine integers. The fact
predicate is
declared to take two arguments, both of type int
, the first an input
argument and the second an output argument.
The definition of fact
uses Prolog syntax for an if/then
statement. It states that if N
is 1
then (the ->
token) the
output variable, X
is 1
. Otherwise (the ;
token), calculate the
factorial recursively using an intermediate variable X0
to hold the
temporary result.
There's a few other ways this could be written. Instead of the Prolog style if/then, we can use an if/then syntax that Mercury has:
fact(N, X) :-
( if N = 1 then X = 1 else fact(N - 1, X0), X = N * X0 ).
Instead of using predicates we can declare fact
to be a function. A function has no output variables, instead it returns a result just like functions in standard functional programming languages. The changes for this are to declare it as a function:
:- func fact(int) = int.
fact(N) = X :-
( if N = 1 then X = 1 else X = N * fact(N - 1) ).
main(!IO) :-
io.print("fact(5, ", !IO),
io.print(fact(5), !IO),
io.print(")\n", !IO)
Notice now that the call to fact
looks like a standard function call and is inlined into the print
call in main
. A final syntactic shortening of function implementations enables removing the X
return variable name and returning directly:
fact(N) = (if N = 1 then 1 else N * fact(N - 1)).
Because this implementation uses machine integers it won't work for values that can overflow. Mercury comes with an arbitrary precision integer module, integer, that allows larger factorials. Replacing the use of the int
module with integer
and converting the static integer numbers is all that is needed:
:- module fact.
:- interface.
:- import_module io.
:- pred main(io::di, io::uo) is det.
:- implementation.
:- import_module integer.
:- func fact(integer) = integer.
fact(N) = (if N = one then one else N * fact(N - one)).
main(!IO) :-
io.print("fact(1000, ", !IO),
io.print(fact(integer(1000)), !IO),
io.print(")\n", !IO).
Conclusion
There's a lot more to Mercury. These are just first steps to test the system works. I'll write more about it in later posts. Some further reading: