[comp.lang.prolog] PROLOG Digest V5 #79

PROLOG-REQUEST@SUSHI.STANFORD.EDU.UUCP (10/25/87)

PROLOG Digest           Wednesday, 28 Oct 1987     Volume 5 : Issue 79

Today's Topics:
                    Query - Algorithmic Debugging,
                       Implementation - Style,
                     LP Library - Textbook Review
----------------------------------------------------------------------

Date: Tue, 20 Oct 87 21:09:36+0900
From: Dongwook Shin <dwshin%csd.kaist.ac.kr@RELAY.CS.NET>
Subject: Algorithmic debugging

BIM_Prolog is advertised to provide rich programming environments.
Especially, BIM_Prolog brochure says that it also include the
algorithmic debugging facility originally devised by Shapiro, without
precise description. Is there anyone out there who knows that this
algorithmic debugging is also applicable to impure Logic Programmings
( with "cut", "assert", and "retract")? Any information would be
appreciated.  Thanks!

-- D.W. Shin

------------------------------

Date: 19 Oct 87 09:23:00 EST
From: <cugini@icst-ecf.arpa>
Reply-to: <cugini@icst-ecf.arpa>
Subject: programming style

We seem to have some people on the list who have strong ideas about
Prolog style.  Here's something that comes up over and over again
in my code and if there's an accepted neat way to handle it, I'd
be happy to know.

The general problem is how to write reasonably efficient code which
smoothly accepts different variables as input or output.

For instance, suppose we have lots of ground facts like this:

  parent_child( big_Lothar, little_Lothar).

with the obvious interpretation, and we want to construct an
ancestor_descendant predicate.  The naive and clean version
might be:

ancestor_descendant(Anc, Des) :- parent_child(Anc, Des).
ancestor_descendant(Anc, Des) :-
  parent_child(Anc, Middle),
  ancestor_descendant(Middle, Des).

This is all just ducky if Anc is ground on invocation.  However, if
we invoke this with Des as the "input" and especially if we want to
get multiple solutions, then of course the code gets inefficient
because it randomly (random wrt the intent) chooses someone as a
possible ancestor and then blindly tries to construct a path to Des.
If we have a couple of hundred ground facts, the inefficiency could
be prohibitive.  After all, a possible Anc fails only after all
his/her descendants have been tried and found wanting (ie not = Des).

Of course we could do some variation of this:

ancestor_descendant(Anc, Des) :- parent_child(Anc, Des).
ancestor_descendant(Anc, Des) :-
  nonvar(Anc) ->
      (parent_child(Anc, Middle),
       ancestor_descendant(Middle, Des)
      );
      (parent_child(Middle, Des),
       ancestor_descendant(Anc, Middle)
      ).

but now we've used a non-logical test and have semi-duplicated code, etc.

Comments ?

-- John Cugini

------------------------------

Date: Tue, 20 Oct 87 23:09:43 PDT
From: quintus!ok@Sun.COM (Richard A. O'Keefe)
Subject: Another textbook

This one is
        @Book(malpas-primer
        ,   Key = Malpas
        ,   Author = "Malpas, John"
        ,   Title = "PROLOG: A Relational Language and its Applications"
        ,   Publisher = Prentice-Hall
        ,   Year = 1987
        )

    Once again, this is NOT a general review of the book.
There are several things about this book that I think are good,
such as the glossary.  Let's briefly comment on that.

    He calls compound terms "structures".  The word "structure"
is indeed used in logic, but it certainly does not mean a
compound term.  The word "structure" is indeed used in
old-fashioned programming, but it doesn't mean a compound term
there either.  Instead it means a record, and records have
mutable Fields which are accessed by symbolic name.  Compound
terms have immutable Arguments which are accessed by position.
Why use a word which manages to mislead both programmers and
logicians when there is a precise term ready to hand which has
no false connotations?

    Yet again, he says that "| is known as the list constructor".
I hope it isn't known as that, because "|" is NOT the list
constructor in most of the Prolog systems Malpas says his book
is relevant to, particularly not in C Prolog, which is the basic
system he worked from.  The list constructor is '.', and in
DEC-10 Prolog, C Prolog, and Quintus Prolog, you can say
        | ?- op(600, xfy, .).
to make expressions such as
        X = a.b.[]
legal.  And even if you don't,
        | ?- functor([a,b], ListConstructor, 2).
binds   ListConstructor = '.'
in those Prologs.

    Is this nit-picking?  Well, no.  Read Ayn Rand's "An Introduction to
Objectivist Epistemology".  It is impossible to think clearly about a
topic if your vocabulary about that topic is muddled.  Anyone who
believes that "|" is the list constructor is in for a nasty shock when
        | ?- functor(List, '|', 2).
fails to construct a list.

    Is this NIH syndrome?  Well, no.  I have several times suggested to
the BSI Prolog group that they should canonise the terminology in John
Lloyd's book, filling in anything else from Robinson's "Logic Form and
Function" or failing that Common Lisp.  (The BSI Prolog group are
inventing a new language, so compatibility appears not to be of interest
to them.)  If you notice me using poor terminology, please let me know,
and I'll mend my ways.  I know that I tend to be rather slipshod about
predicate -vs- procedure, and it's not good enough!

    Another good feature of the book is its catalogue of Prolog
implementations.  It is one of the few books about real Prolog to tell
you about Turbo Prolog.

    There are other good things I could say about this book.  So let's
get on with the criticism.

    My touchstone for evaluating the quality of a Prolog textbook is to
see what it says about all-solutions predicates.  (Don't forget that
part of the point of these two textbook reports is to justify my claim
that Lagache's difficulties with findall&Co are no worse than some
textbooks.)

    setof/3 and bagof/3 are not in the index of this book.  findall/3,
however, is.  Page 338 provide what is described as "source code for a
version of findall".  Here is the code.


        findall(Element, Query, _) :-
                Query,
                gettmplist(List),
                append(List, [Element], Newlist),
                assert(tmplist(Newlist)),
                fail.
        findall(_, _, List) :-
                retract(tmplist(List)),
                !.

        gettmplist([]) :-
                \+ tmplist(_),
                !.
        gettmplist(List) :-
                retract(tmplist(List)),
                !.

        unique_value_list(Element, Query, _) :-
                Query,
                gettmplist(List),
                (   member(Element, List),
                    assert(tmplist(List))
                ;   \+ member(Element, List),
                    assert(tmplist([Element|List]))
                ),
                fail.
        unique_value_list(_, _, List) :-
                retract(tmplist(List)),
                !.

Ouch!

    It is interesting that this is the only version of findall/3 I have
ever come across which fails when there are no solutions.  Suppose that
the dynamic predicate tmplist/1 is initially empty.  Then we trace
    ?- findall(*, fail, List)
        Clause 1 calls fail, which fails.
        Clause 2 calls retract(tmplist(List)), which fails.
       findall/3 fails.
The correction for this, of course, would be to change the last clause
of findall/3 to
        findall(_, _, List) :-
                gettmplist(List).
and make the corresponding change to unique_value_list/3.  Of course,
this version of findall/3 is not steadfast.  If you call
    ?- findall(*, true, [X,Y]).
the second clause of findall/3 will fail, because
        retract(tmplist([X,Y]))
will <<correctly>> fail to recognise the clause
        tmplist([*])
findall/3 having failed, tmplist([*]) will be left in the data base.
So if we then call findall/3 again in utter disbelief, we'll get the
incorrect answer X = *, Y = *.  Try it!  The correction for this would
be to make gettmplist/1 steadfast.

        gettmplist(List) :-
                retract(tmplist(L)),
                !,
                List = L.
        gettmplist([]).

Having fixed two bugs, we are then left with the problem that this
version of findall/3 will not work if calls to findall/3 can be nested.
As indeed they can and should be.  This is not the only textbook with
an all-solutions predicate which cannot be nested.  I do not understand
this.  Why would anyone fail to worry about nested calls to control
structures?  Well, we can hack around this bug by temporarily moving
the outer value out of the way.

        findall(Template, Generator, List) :-
                retract(tmplist(OuterList)),
                !,
                findall_1(Template, Generator, L),
                assert(tmplist(OuterList)),
                List = L.
        findall(Template, Generator, List) :-
                findall_1(Template, Generator, List).

        findall_1(Template, Generator, List) :-
                /* what findall/3 used to be */

That only leaves us with efficiency problems (I think.  I haven't
really been able to stomach testing it.)

One source of inefficiency is the way the list is built up using
append/3.  If the final list has N elements, we'll have done
about (1/2)N^2 calls to append.  This is in fact pretty much what
naive reverse does.  If instead, the list were built up by adding
elements at the front and then reversing the final list, list
manipulation would take O(N) time.

The preferred way of building a list in Prolog is
to calculate L = [L1,...,Ln] do
        calculate_list([], LoopVars) :-
                ended(LoopVars).
        calculate_list([Element|Elements], LoopVars) :-
                calculate_next_element(LoopVars, Element),
                advance_loop_variables(LoopVars, NextVars),
                calculate_list(Elements, NextVars).
Where you can't do this,
        calculate_list(List, InitVars) :-
                calculate_list([], Rev, InitVars),
                reverse(Rev, List).

        calculate_list(L, L, LoopVars) :-
                ended(LoopVars).
        calculate_list(L0, L, LoopVars) :-
                calculate_next_element(LoopVars, Element),
                advance_loop_variables(LoopVars, NextVars),
                calculate_list([Element|L0], L, NextVars).

In some implementations of Prolog, this may be enough to take the
cost down to O(|Size of answer List|) time.  But not in any of the
Prolog implementations mentioned by Malpas.  What is the reason for
that?  If you have a List of size |List| in the data base, and an
Element of size |Element| you want to add to it,
        | ?- retract(tmplist(List)),
             !,
             assert(tmplist([Element|List])).
will cost O(|List|+|Element|) space and time in a structure-sharing
system, and O(2*|List|+|Element|) space and time in a structure-
copying system.  retract/1 in effect makes a copy of the clause it
deletes.  In a structure-sharing system, this is pretty cheap, at
the price of delaying the reclamation until there are no further
references to any part of the deleting clause.  In a structure-copying
system, it takes O(|List|) time and space to build the copy.  Some
systems might do better if substantial parts of the clause are ground.
Then assert takes O(|List|+|Element|) time and space to build a copy
of the new term.  This has to be done so that any variables in the term
will be properly handled.  Again, if substantial parts of the new
clause are ground some systems may do better.

With all these copies going on, the cost of this version of findall/3
is O(length of final result*size of final result).  Whew!

If you are going to maintain a stack or an output-restricted deque
in the data base, do it by using
        asserta/1       to add items at the head of the deque
        assertz/1       to add items at the foot of the deque
        retract_first/1 to remove items from the head of the deque.
DON'T try to view the thing as a single "variable" held in a single
clause, because updating that "variable" will be appallingly costly in
nearly every Prolog system.

    I don't know where Malpas got this code, or why he chose to
recommend it to his readers rather than the version in Clocksin &
Mellish.  It is pretty bad.

    Have any Prolog Digest readers any experience with this book,
either learning Prolog from it yourself, or trying to teach a class
with it?  What do you think are the particularly good features of
the book?  Do you think it matters that the code given in an
appendix for an operation already available in some form in most of
the Prolog systems he cares about is poor?  Are there any examples
in the book which you think are particularly helpful?

    Final repetition: this is not a general review of the book, and
there are good things in it too.

------------------------------

End of PROLOG Digest
********************