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.
Let us suppose that Prolog's designers had never invented
the arithmetic operator
is. Assume instead that they
gave us predicates
performing arithmetic operation on its first
and returning the result in the third. Then instead of
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
this inconvenience when doing arithmetic. But when
we want to manipulate sets, vectors, complex numbers, lists
indeed, any time we just want to nest
we are still forced to do this unwrapping.
So I have written a preprocessor named
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.
grips is my evaluation operator.
It tells the translator to generate a goal that evaluates
N-1, and prepend it to the call of
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
as a function, promoting functional
evaluation to cover all of the body, as it were.
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:
I do this as follows. If the expression is a number,
return it as the result. The goal doesn't need to do
Otherwise, assume the
is a function call. Split this into the function,
arguments. Translate the arguments into a goal
them, together with a variable to hold the result of
evaluation. Then conjoin to
ArgsGoal another goal
FGoal, formed by calling
F on a
of the evaluated arguments to which a result variable
Here are some examples. To translate
code is needed to evaluate the arguments, so
true. The goal
FGoal is the function
— called on the evaluated arguments with a result variable
In this case, the evaluated arguments are the same as the
the result variable.
For the more complicated expression
plus( mult(1,2), 3
The evaluated arguments becone
[ R1, 3 ],
the first one is replaced by the variable holding the result
evaluating it. The goal
plus( R1, 3
finally, the goal for evaluating the entire expression becomes
mult( 1, 2, R1 ), plus( R1, 3, R )
Here is the translation code. The predicate
translates the expression in its first argument into a
its second and a goal in its third. It calls
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
are variables. These shouldn't occur
as the argument to
but will turn up when Grips translates definitions, which
could contain variables (possibly from their head) in the
expression, in the way that the second clause for
fac( N ) <- N * fac( N-1 ).
If you try this code, you will find the generated goals
a good deal of unnecessary
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.
contains several preprocessor hooks: predicates that
define in order to make the compiler
preprocess various syntactic entities in your
These are not in
ISO Prolog standard, but several other implementations, such
also provide them. If your Prolog doesn't, you will have to
another way to hook your preprocessor into it. The easiest
write your own version of
I use the
hook to add the
macro mentioned earlier. As it reads a
SWI-Prolog hands each goal
G appearing in the body of a
goal_expansion, passing it as the first
the call succeeds,
G gets replaced by
second argument, assumed to be its translation.
This is how I make
First, I write a predicate to translate goals whose
could contain a
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
trans_expron 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
to test whether it is dealing with a
I took care not to write the
=.. to test for it. If I hadn't,
then if I already had the preprocessor installed (which I
if repeatedly editing, cnsulting, and testing it), it would
expanding this particular
amusing but non-terminating results.
Now I can connect
goal_expansion, by writing
a clause for the latter.
I make this clause test whether the goal actually
— whether it does contain a
grips — and fail
This is good practice, because some Prologs might call
over and over again on the same goal if I make it return
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
goal_expansion in force at the same time if you
someone else have installed other preprocessors. You'll
to ensure these work correctly together, especially if a
goal contains constructs from different preprocessors, or
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
Ras 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
:- 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 ].
In the same kind of way that I connected
I can now connect
translates complete terms in the source file.
to use than
I can test whether a definition needs translating
by whether it contains a
<- connective; I
need to decompose definitions in the same way I did with
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
in the same way as numbers. Non-empty lists, it
evaluates element by element.
One construct that I added is an
;. The expression
E1 and returns its result if it
succeeds, otherwise returning that of
Turning to arithmetic, Grips
* and other
operators into calls to
find this terribly convenient.
We do need to take care with the semantics. One question is
distinguish between atom constants and functions of no
For example, how should we invoke
We can't write
read(), because it's not valid
But if we make Grips take the atom
read to denote
to the predicate, how can we write the atom constant
On the other hand, if
read denotes the atom,
need a special notation for the function call. I
decided that an atom on its own should be a call, and that
it a literal, I should precede it by a
In fact, I use
` to quote
term, not just atoms.
A different question: what about the evaluation operator
translator recognise it if it occurs at any level inside a
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.