[net.lang.prolog] Imperative Prolog, P

OKeefe.R.A.%EDXA@sri-unix.UUCP (11/07/83)

From:  O'Keefe HPS (on ERCC DEC-10) <OKeefe.R.A. at EDXA>

     Logic programs are just as much algorithms as assembly code.  So
we should distinguish not between "algorithmic" and "pure", but 
between "imperative" and "applicative" uses of Prolog.  That aside, I
would like to agree with Russ Abbott.  [How's that for a record ?]  
There is no need for us to invent an imperative version of Prolog.  
Dijkstra has already done it for us.  We have only to add a few new 
data structures.

     There are two good reasons why most Prologs don't accept terms 
like P(X...Z).  The first is that it is not at all clear what they 
mean.  'apply' is defined in the DEC-10 Prolog library as

        apply(Pred, Args) :-
                atom(Pred), !,
                Goal =.. [Pred|Args],
                call(Goal).
        apply(Pred, Args) :-
                Pred =.. List,
                append(List, Args, Full),
                Goal =.. Full,
                call(Goal).

modulo details.  The point of this is to give us partial application,
which is how we get the effect of higher-order functions without the
need for any other extensions to the language.  [Re dictating to the
user: neither David Warren nor I said to the user "thou shalt not use
higher-order code".  His paper pointed out "you've already got the
tools you need, go to it."]  So we very often want P to have some of
its arguments already filled in.  Since

        apply(p, [a,b,c])
        apply(p(a), [b,c])
        apply(p(a,b), [c])
        apply(p(a,b,c), []) 

all have exactly the same effect (modulo calls to append), we would like

        P1 = p, P1(a,b,c)
        P2 = p(a), P2(b,c)
        P3 = p(a,b), P3(c)
        P4 = p(a,b,c) P4

to have the same effect.  Since the terms in the right hand column are
all supposed to be the same goal, we would like them to unify.  That's
not too hard to arrange for P1(a,b,c) and P4, but rather harder for
the others.  Before anyone points out that p(a,b,c) and
apply(p,[a,b,c]) don't unify either, let me point out that 
apply(p,[a,b,c]) makes no pretence of BEING the goal p(a,b,c), but
only claims to CALL it.

     Still on logical difficulties, if P(X...Z) is accepted as a term,
one might expect to match it against something in order to bind P.
Again, we already have

        P4 = p(a,b,c) working, and it wouldn't be too hard to make

        P1(a,b,c) = p(a,b,c)

work.  Indeed, you can tell Poplog that you want this to happen.  But
P2 and P3 give you trouble.  Restricting "predicate variables" to take
on atomic values only just will not do: my experience has been that I
have wanted to pass around a goal with some of its arguments filled in
much more often than I have wanted to pass a goal with none of its
arguments specified.  For example, to add N to all the elements in a
list, one might write

        ... maplist(plus(N), L, Incremented), ...

     Poplog permits P$[X,...,Z], and {SU-SCORE}PS:<Prolog>Read.Pl 
permits P(X,...,Z) -- against my better judgement -- as notational 
variants for apply(P, [X,...,Z]).  The Poplog form, where $ is just an
infix 'apply', is a lot cleaner.  Of course neither of these makes
much sense except as a goal, and when P is bound to 'p' P$[a,b,c]
claims to be the goal p$[a,b,c] not the goal p(a,b,c).

     The second good reason is: how are such terms to be stored ?  
Many Prologs use a very space-efficient scheme where f(X1,...,Xn) is 
represented by n+1 consecutive pointers.  The 1st .. nth pointers are
or point to the arguments, while the 0th pointer points to a "functor
block" representing f/n, so that there is no need to store the arity
with the term.  This might be thought to lead to more page references,
but there is an obvious little trick to reduce the impact of that.
The representation used by Poplog is {X1 ... Xn f}, which turns out to
require at least two more words per term.  Since most terms are fairly
short, this is a substantial overhead.  Poplog represents Prolog lists
as Pop pairs which makes the space cost the same for both methods, but
the price is a time cost on *everything* that looks at terms, even
when they aren't lists.  The Poplog system can however handle P4 as
well as P1.  Prologs embedded in Lisp commonly represent Prolog terms
as lists.  With CDR-coding, that is as efficient as Poplog.
Presumably LM-Prolog does this.  If you lack CDR-coding, as
micro-PROLOG does, the result is that handling P(X...Z) as (P X ... Z)
is trivial, indeed it is almost inescapable, but that the space cost
is appalling.

     Of course a mixed representation could be used, with most terms
being represented efficiently as <^f/n, X1, ..., Xn>, and P(X1..Xn)
terms being handled as '$$'(P,X1,...,Xn).  We could arrange that if
unification of two compound terms failed, it would check for either or
both of the terms being '$$', and if so would flatten them, E.g.
'$$'(p(a,b),c) => p(a,b,c), and try again.  We would also arrange to
have machine code definitions for all '$$'/N (whenever a clause is
asserted for F/N, ensure that $$/1 .. $$/N+1 are all defined) with the
effect of

        '$$'(Pred, A1, ..., An) :-
                call Pred with extra args A1 .. An.  

where of course Pred might be another $$ term.

     Now I would certainly agree that having a family of predicates

        apply(p(X1,..,Xk), Xk+1, ..., Xn) :-
                p(X1, ..., Xk, Xk+1, ..., Xn)

efficiently implemented in machine code so that all these wretched
lists weren't created only to be immediately thrown away would be a
good thing.  But I cannot see why writing

        apply(P, X, Y, Z) or P$[X, Y, Z]

is thought to be an outrageous demand on the programmer, and

        P(X, Y, Z)

is thought to be so much more attractive.  Whether Prolog **ought** to
make a syntactic distinction between goals and terms is not yet at
issue, the fact is that is *doesn't*, and if we are to adopt a
notation for one use, it ought to make sense in the other..  The
implementation cost of making P2(b,c) = P3(c) work is far too high for
my liking, and the increased complexity in the language in either case
is unacceptable.

     Here is a quotation from Hoare, "Hints on Programming Language 
Design", which explains my attitude better than I can.

     A necessary condition ... is the utmost simplicity in the design
of the language.  ... But the main beneficiary of simplicity is the
user of the language.  In all spheres of human intellectual and
practical activity, from carpentry to golf, from sculpture to space
travel, the true craftsman is the one who thoroughly understands his
tools.  And this applies to programmers too. ...

     It therefore seems especially necessary in the design of a new
programming language ... to pursue the goal of simplicity to an
extreme, so that a programmer can readily learn and remember all its
features, can select the best facility for each of his purposes, can
fully understand the effects and consequences of each decision, and
can then concentrate the major part of his intellectual effort to
understanding his problem and his programs rather than his tool.

[This is the paper where he says of Fortran "The standardizers have 
maintained the horrors of early implementations, and have resolutely 
set their face against the advance of language design technology, and
have thereby saved it from many later horrors."  I wonder what he'd
say now of Fortran 8x?!  Save Prolog from horrors!]