Although Prolog can be remarkably concise, it's far from ideal when we want to compose function calls. Even an expression as simple as "length of list A appended to list B" must be unwrapped into "append A to B giving C, then return the length of C". I'll explain how I overcame this by writing a preprocessor that translates functional notation into Prolog. It lets me define predicates as if defining functions, and write nested function calls without having to do what Fortran implementors had taught their compilers to do in the 1950s. The version described here works with SWI-Prolog.
goal_expansion
term_expansion
Let us suppose that Prolog's designers had never invented
the arithmetic operator is
. Assume instead that they
gave us predicates
such as
plus
and mult
,
each
performing arithmetic operation on its first
two arguments
and returning the result in the third. Then instead of
writing
X is A*B + A*C.we would have to unwrap the expression so it becomes
mult( A, B, V1 ), mult( A, C, V2 ), plus( V1, V2, X ).
Fortunately, we do have is
, which
spares us
this inconvenience when doing arithmetic. But when
we want to manipulate sets, vectors, complex numbers, lists
—
indeed, any time we just want to nest
function
calls
—
we are still forced to do this unwrapping.
So I have written a preprocessor named
"Grips" that
does it for me. One of its benefits
is that I can pass arithmetic expressions as arguments to goals,
avoiding the eternal NextLength is Length + 1
's and
NextCounter is Counter-1
's that disfigure so many
programs. As an example, instead of defining "factorial" as
fac( 0, 1 ). fac( N, F ) :- Nd is N-1, fac( Nd, Fd ), F is Fd * N.I can code its second clause as
fac( N, F ) :- fac( grips N-1, Fd ), F is Fd * N.
Here, grips
is my evaluation operator.
It tells the translator to generate a goal that evaluates
N-1
, and prepend it to the call of fac
.
Indeed, I can move the evaluation operator further out and define:
fac( N, F ) :- F is N * grips( fac( N-1 ) ).Grips knows about
is
, so it
will correctly translate this mixture
of arithmetic operators and ordinary predicates.
I can also write fac
as a function, promoting functional
evaluation to cover all of the body, as it were.
Here is fac
defined in this way:
fac( 0 ) <- 1. fac( N ) <- N * fac( N-1 ).
In the rest of the essay, I'll explain how I implemented Grips for SWI-Prolog. Some of the techniques may be useful if you want to write other preprocessors.
The idea is to translate each expression into a pair consisting of a goal and a "result". The result will be the original expression if that needs no evaluation. Otherwise it will be a variable which the goal will bind to the result of evaluation. For example:
Expression | Goal | Result |
1 |
none | 1 |
f(1) |
f(1,R) |
R |
f(g(1),h(2)) |
g(1,V1),h(2,V2),f(V1,V2,R) |
R |
I do this as follows. If the expression is a number,
return it as the result. The goal doesn't need to do
anything, so
make it true
.
Otherwise, assume the
expression
is a function call. Split this into the function, F
, and
its
arguments. Translate the arguments into a goal ArgsGoal
which
evaluates
them, together with a variable to hold the result of
evaluation. Then conjoin to ArgsGoal
another goal
FGoal
, formed by calling F
on a
list
of the evaluated arguments to which a result variable
is appended.
Here are some examples. To translate
plus(1,2)
, no
code is needed to evaluate the arguments, so ArgsGoal
becomes
true
. The goal FGoal
is the function
F
—
that is,
plus
— called on the evaluated arguments with a result variable
appended.
In this case, the evaluated arguments are the same as the
originals,
so FGoal
becomes plus(1,2,R)
, where
R
is
the result variable.
For the more complicated expression plus( mult(1,2), 3
)
,
ArgsGoal
becomes
mult(1,2,R1)
.
The evaluated arguments becone [ R1, 3 ]
,
where
the first one is replaced by the variable holding the result
of
evaluating it. The goal FGoal
becomes plus( R1, 3
)
. And
finally, the goal for evaluating the entire expression becomes
ArgsGoal
conjoined
with FGoal
, or
mult( 1, 2, R1 ), plus( R1, 3, R )
Here is the translation code. The predicate
trans_expr
translates the expression in its first argument into a
result in
its second and a goal in its third. It calls
trans_arglist
to translate function arguments:
trans_expr( Expr, Expr, true ) :- number( Expr ), !. trans_expr( Expr, Expr, true ) :- var( Expr ), !. trans_expr( Expr, ResultVar, Goal ) :- Expr =.. [ F | Args ], trans_arglist( Args, ArgResults, ArgGoals ), append( ArgResults, [ResultVar], ArgsAndResultVar ), FGoal =.. [ F | ArgsAndResultVar ], Goal = ( ArgGoals , FGoal ). trans_arglist( [ Arg1 | Args ], [ Result1 | Results ], Goal ) :- trans_arglist( Args, Results, Goals ), trans_expr( Arg1, Result1, Goal1 ), Goal = ( Goal1 , Goals ). trans_arglist( [], [], true ).
I have added a clause that checks for expressions
that
are variables. These shouldn't occur
as the argument to
grips
,
but will turn up when Grips translates definitions, which
could contain variables (possibly from their head) in the
tail
expression, in the way that the second clause for
factorial
above did:
fac( N ) <- N * fac( N-1 ).
If you try this code, you will find the generated goals
contain
a good deal of unnecessary true
s. By
writing a goal-conjoining predicate, I
made Grips remove these:
conjoin( true, G, G ) :- !. conjoin( G, true, G ) :- !. conjoin( G1, G2, (G1,G2) ).This is a good utility to have whenever we write programs that code-generate Prolog.
goal_expansion
SWI-Prolog
contains several preprocessor hooks: predicates that
you can
define in order to make the compiler
preprocess various syntactic entities in your
code.
These are not in
the
ISO Prolog standard, but several other implementations, such
as
SICStus,
also provide them. If your Prolog doesn't, you will have to
find
another way to hook your preprocessor into it. The easiest
is to
write your own version of consult
.
I use the goal_expansion
hook to add the
grips
macro mentioned earlier. As it reads a
file,
SWI-Prolog hands each goal G
appearing in the body of a
clause to
goal_expansion
, passing it as the first
argument. If
the call succeeds, G
gets replaced by
goal_expansion
's
second argument, assumed to be its translation.
This is how I make goal_expansion
act.
First, I write a predicate to translate goals whose
arguments
could contain a grips
:
trans_goal( G, GTranslated ) :- G =.. [ F | Args ], trans_goal_args( Args, ArgResults, ArgGoals ), FGoal =.. [ F | ArgResults ], GTranslated = ( ArgGoals , FGoal ). trans_goal_args( [], [], true ) :- !. trans_goal_args( [Arg1|Args], [Result1|Results], Goal ) :- trans_goal_arg( Arg1, Result1, Goal1 ), trans_goal_args( Args, Results, Goals ), Goal = ( Goal1 , Goals ). trans_goal_arg( Arg, Result, Goal ) :- nonvar( Arg ), Arg =.. [ grips , E ], !, trans_expr( E, Result, Goal ). trans_goal_arg( Arg, Arg, true ).This predicate,
trans_goal
, runs over the
arguments of
a goal G
. If any argument is a term
grips(E)
,
trans_goal
calls trans_expr
on the expression E
. It
collects
the
goals for evaluating the arguments and prepends them to
G
.
Thus, the
goal
write( grips( plus(1,2) ) )gets transformed into
plus( 1, 2, R ), write( R ).
Notice that the first clause to
trans_goal_arg
needs
to test whether it is dealing with a grips(E)
.
I took care not to write the grips(E)
explicitly, instead
calling =..
to test for it. If I hadn't,
then if I already had the preprocessor installed (which I
might
if repeatedly editing, cnsulting, and testing it), it would
try
expanding this particular grips(E)
, with
amusing but non-terminating results.
Now I can connect trans_goal
to goal_expansion
, by writing
a clause for the latter.
I make this clause test whether the goal actually
needs translating
— whether it does contain a grips
— and fail
if
not.
This is good practice, because some Prologs might call
goal_expansion
over and over again on the same goal if I make it return
the original
without any change. Here is the code:
needs_translating( G ) :- nonvar( G ), G =.. [ _ | Args ], member( Arg, Args ), nonvar( Arg ), functor( Arg, grips, 1 ), !. :- multifile user:goal_expansion/2. :- dynamic user:goal_expansion/2. user:goal_expansion( G, GTranslated ) :- needs_translating( G ), !, trans_goal( G, GTranslated ).As with
trans_goal
, I've taken care to avoid an
explicit
grips(E)
in the code.
There could be could be several different
clauses for
goal_expansion
in force at the same time if you
or
someone else have installed other preprocessors. You'll
need
to ensure these work correctly together, especially if a
single
goal contains constructs from different preprocessors, or
gets translated
by one preprocessor into a construct handled by another.
Translating function definitions is now straightforward. To translate
double(N) <- plus(N,N).I translate
plus(N,N)
, giving the goal
plus(N,N,R)
.
I use this as the tail of the new predicate. I then insert
R
as the final argument of double(N)
. And
lo and behold:
double( N, R ) :- plus( N, N, R ).
Here is the translation code, in which the main predicate
is trans_def
:
:- op( 1200, xfx, user:(<-) ). trans_def( (Head <- Expr) , (PrologHead:-Tail) ) :- !, trans_expr( Expr, ResultVar, Tail ), trans_head( Head, ResultVar, PrologHead ). trans_head( Head, ResultVar, PrologHead ) :- Head =.. [ F | Args ], append( Args, [ResultVar], ArgsAndResultVar ), PrologHead =.. [ F | ArgsAndResultVar ].
term_expansion
In the same kind of way that I connected
trans_goal
to goal_expansion
,
I can now connect trans_def
to the term_expansion
preprocessor hook.
This resembles
goal_expansion
, but
translates complete terms in the source file.
For Grips, term_expansion
is simpler
to use than
goal_expansion
:
I can test whether a definition needs translating
just
by whether it contains a <-
connective; I
don't
need to decompose definitions in the same way I did with
goals;
and I don't need to worry about avoiding explicit calls to
<-
in the translator. Here is the code:
:- multifile user:term_expansion/2. :- dynamic user:term_expansion/2. user:term_expansion( Def, DefTranslated ) :- trans_def( Def, DefTranslated ).
For real-world applications, the code
above will need
extending. For example, I have made Grips treat the empty-list atom
[]
as a constant that stands for
itself
in the same way as numbers. Non-empty lists, it
evaluates element by element.
One construct that I added is an
expression-disjunction
operator named ;
. The expression
E1;E2
evaluates E1
and returns its result if it
succeeds, otherwise returning that of E2
.
Turning to arithmetic, Grips
translates +
, *
and other
arithmetic
operators into calls to is
. I
find this terribly convenient.
We do need to take care with the semantics. One question is
how to
distinguish between atom constants and functions of no
arguments.
For example, how should we invoke read/1
in
functional notation?
We can't write read()
, because it's not valid
syntax.
But if we make Grips take the atom read
to denote
a call
to the predicate, how can we write the atom constant
read
?
On the other hand, if read
denotes the atom,
then we
need a special notation for the function call. I
eventually
decided that an atom on its own should be a call, and that
to make
it a literal, I should precede it by a
backquote operator `
.
In fact, I use `
to quote
any
term, not just atoms.
A different question: what about the evaluation operator
grips
?
Should the
translator recognise it if it occurs at any level inside a
goal,
or only at the top level? That is, should the goal
write( pair( grips( plus(1,2) ), 3 ) )stay as it is, or evaluate to
write( pair( 3, 3 ) )
?
I use Grips more than I do Definite Clause Grammars. It is just so convenient when writing complicated expressions. It also saves thinking up names for result variables, thus mitigating the psychological disorder known as "naming fatigue" — a steadily increasing inability, as the day wears on, to invent meaningful identifiers.
Functional notation is also wonderful because it makes data flow explicit. When reading a goal such as
bar( C, D, A ), foo( A, B ), fred( B, D )one has to examine the predicate definitions and accompanying comments or mode declarations before being sure of the data flow. But the same call in functional notation makes the flow immediately clear:
fred( foo( bar(C,D) ), D )So I have used functional notation in teaching Prolog, to make it easier for novices to read Prolog code.