Path: utzoo!utgpu!water!watmath!clyde!att!osu-cis!tut.cis.ohio-state.edu!mailrus!cornell!uw-beaver!teknowledge-vaxc!sri-unix!quintus!ok From: ok@quintus.uucp (Richard A. O'Keefe) Newsgroups: comp.lang.prolog Subject: Re: The double-cut (long) Message-ID: <265@quintus.UUCP> Date: 9 Aug 88 21:08:02 GMT References: <1164@ttds.UUCP> Sender: news@quintus.UUCP Reply-To: ok@quintus.UUCP (Richard A. O'Keefe) Organization: Quintus Computer Systems, Inc. Lines: 204 In article <1164@ttds.UUCP> jonasn@ttds.UUCP (Jonas Nygren) provides an example which explains why he wanted double-cut. In this message, I go through that example and show why it does not need the double-cut. >Instead of inventing my own example I choose to base it on an example most of >you have seen, the reconsult example in Clocksin & Mellish. I have done some >smaller modifictions to the code. .. > >myreconsult(File) :- > retractall(done(_)), > assert(done(1)), %<-- This seems to be a mistake > seeing(Old), > see(File), > repeat, > read(Term), > try(Term), > seen, > see(Old), > !. The "assert(done(1))" is not present in Clocksin & Mellish, and doesn't seem to accomplish anything. (Since done(Skel) is used to record that a predicate has been loaded, it can be argued that assert(done(1)) is in some sense ill-typed. If there is some Prolog system which is so very badly broken that it _needs_ a dummy clause, assert(done(fail)) might be slightly better taste. Best of all, don't use broken tools.) Apart from that, the code is verbatim from Clocksin & Mellish. They have the cut in the wrong place. You should always put the cut of a repeat loop as early as possible. reconsult(File)) :- retractall(done(_)), seeing(Old), see(File), repeat, read(Term), try(Term), !, seen, see(Old). >try(X) :- is_eof, !. >try((?- ...Goals)) :- !, do_call(Goals), fail. >try(Clause) :- > head(Clause,Head), > record_done(Head), > assertz(Clause), > fail. >do_call([]) :- !. >do_call([G|Goals]) :- call(G), do_call(Goals), !. OUCH! What IS this? The code in Clocksin & Mellish was try((?-Goals)) :- !, call(Goals), !, fail. (Which isn't quite what Prolog is supposed to do, but we'll get to that.) Assuming that there is a Prolog system which reads the query ?- p(X), q(X) as ?-(...([p(X),q(X)])), which is a strange thing for it to do, we shouldn't need the cuts in do_call, but should write ... try((?- ... Goals)) :- !, call_list(Goals), !, fail. ... call_list([]). call_list([Goal|Goals]) :- call(Goal), call_list(Goals). Note that by putting the cut where it belonged (which is where C&M put it), we have ended up with a much more useful predicate: call_list/1 is more generally useful than do_call/1. >record_done(Head) :- done(Head), !. >record_done(Head) :- > functor(Head,Func,Arity), > functor(Proc,Func,Arity), > asserta(done(Proc)), > retractall(Proc), > !. Note that the cut at the end of the second clause here is utterly and completely useless: the clause is determinate anyway! >head((A:-B), A) :- !. >head(A,A). >The cuts in 'do_call','record_done' and 'head' are due to the fail in 'try' The cut in head/2 has nothing whatsoever to do with the fail in try/1. That cut is needed because head/2 is a disguised if->then;else : head((Head :- Body), Head). head(Unit, Unit) :- Unit ~= (_ :- _). There are two cuts in record_done/1. As we just saw, one of them is totally pointless. The other one is again implementing if->then;else: record_done(Head) :- done(Head). record_done(Head) :- \+ done(Head), same_functor(Head, Skel), retractall(Skel), assert(done(Skel)). Efficiency is not a concern here, so we might as well leave it in if-then-else form. We saw that the two cuts in do_call/2 were in the wrong place (actually, the first of them accomplishes nothing) and should be replaced by a single cut in try/3. >which means that the complexity of 'try' (2nd & 3rd clause) has propagated to >their subgoals. Using the double-cut as proposed would allow us to limit >the complexity to the clause where it arises: We've seen that this conclusion is false. [Pause to check through various Prolog manuals.] The ... and list appear to come from AAIS Prolog. **UGH**! Page 139 of the AAIS manual says that read/1 returns the atom 'end_of_file' at the end of a file, so we have that much Edinburgh compatibility. Strictly speaking, the definition in Clocksin & Mellish is incorrect: if a query in a file fails, a Prolog system is supposed to print a warning message. So we can write reconsult/1 thus: reconsult(File)) :- retractall(done(_)), seeing(Old), see(File), repeat, % repeat and cut form an inseparable read(Term), % pair of brackets, *never* have a process(Term), % repeat-loop without its cut. !, seen, see(Old). process(end_of_file) :- !. % This is a guard/commit cut process((?- ... Goals)) :- !, % This is a guard/commit cut ( call_list(Goals) -> true % Execute the goals, but if they fail ; write(user_output, 'Command failed: '), writeq(user_output, (?- ... Goals)), nl(user_output) ), fail. process(Clause) :- clause_parts(Clause, Head, Body), maybe_retractall(Head), assertz(Clause), fail. call_list([]). call_list([Goal|Goals]) :- call(Goal), call_list(Goals). clause_parts((Head :- Body), Head, Body). clause_parts(Unit, Unit, true) :- Unit \= (_ :- _). maybe_retractall(Head) :- done(Head). maybe_retractall(Head) :- \+ done(Head), functor(Head, F, N), functor(Skel, F, N), same_functor(Head, Skel), retractall(Skel), assert(done(Skel)). Summary: one repeat-fail-! loop (avoidable by using recursion) one guard/commit cuts (due to the defaulty input representation, avoidable in the general case by elementary data structure design) one if->then;else due to reconsult's idiosyncratic rules about queries in files one if-then-else due to the defaulty representation of clauses one if-then-else required to implement the rule "don't clear out a predicate if you have already seen it". Not *ONE* of these cuts is required "to prevent backtracking over builtins", and in *NONE* of these cases is the double-cut required or even useful. >In this code no cuts are needed in 'dc_do_call', 'dc_record_done' or >'dc_head' and the fail-complexity is totally contained in 'dc_try'. >Mr O'Keefe it's not a question of necessity but of beauty and functionality. >In the last example the number of cuts has actually diminished with use of >the double-cut and the code is 'cleaner' and easier to understand, I believe. This is wrong. dc_head is simply incorrect as it stands. According to the code as Nygren wrote it, the head of (p :- q) is (_ :- _). Oh, it _also_ says that the head is p, but it _will_ deliver the wrong answer if given a chance. Similarly, dc_record_done is quite happy to clear out a predicate a second time (and add done(...) a second time to the data base), if it is given the chance. These operations can no longer be read on their own: to understand how dc_head or dc_record is supposed to work, you have to check EVERY place in the program that calls them, to make sure that there is a cut following (or even obscurer, preceding in the guise of a double-cut). I think that the code as I wrote it is much clearer. In particular, you do not have to examine the callers of clause_parts/3 to see that it is determinate. To paraphrase the Bible, parents should not be cut for the sins of their children, but each clause should be cut (if at all) for its own sins.