• Aucun résultat trouvé

The cut command

Dans le document Prolog and Natural-Language Analysis D (Page 126-130)

4.3 Problem Section: Grammar Extensions

5.1.2 The cut command

The behavior of a Prolog program is based on the depth-first, backtracking control regime that the Prolog system follows. The Prolog interpreter will explore the entire space of backtracking alternatives in search of a solution to a goal. This behavior, al-though simple and (in the limit) complete, sometimes has undesirable consequences.

In this section we present a metalogical facility that changes the control regime of a Prolog program and that can be used for several purposes including increasing effi-ciency, eliminating redundancy, and encoding conditionals.

The cut command, notated by an exclamation mark “!”, is used to eliminate branches of the search space. We refer to cut as a command, rather than a predicate or operator, to emphasize that it does not fit smoothly into the logical view of Prolog, and cannot be felicitously thought of as a predicate which holds or does not hold of arguments.

The clauses for a predicate give alternative ways of proving instances of that pred-icate. In proving a goal, Prolog chooses each alternative in turn until one leads to a proof of the goal. The cut command always succeeds, but as a side effect it makes some of the current clause choices permanent for the duration of the proof. Specifically, if a clause

p :- g1,. . .,gi,!,. . .,gn.

is being used to prove an instance of p and the cut is reached, then the choice of this clause to prove that instance of p, as well as all choices of clauses in proving g1through gi, are made permanent for the duration of the overall proof of which the proof of p is a part.

Another way of looking at the action of the cut command is as the converse of the previous statements, that is, by examining which proofs the cut eliminates. When Prolog backtracks to find an alternative proof of an occurrence of cut, not only is no other proof found for the cut instance but also the whole goal that invoked the clause with this cut instance is taken to have no proof (even if other clause alternatives might lead to such proof in the absence of the cut).

The cut command can be used in several ways. If we have a series of clauses for a particular predicate which we happen to know are all mutually exclusive, then once one of the clauses has succeeded, there is no use in attempting to find other solutions; all other branches in the search space will ultimately fail. We can eliminate

5.1. Metalogical Facilities

This digital edition of Prolog and Natural-Language Analysis is distributed at no charge for noncommercial use by Microtome Publishing.

117

the inefficiency engendered by searching these blind alleys by using cut to force the clause choice.

For instance, consider the definition of amax_valuedpredicate which holds of a nonempty list of terms and the term in the list with highest valuation according to a binary comparison predicatehigher_valued. (We assume that all objects in the list have distinct valuations; there must be no “ties”.) Such a predicate might be used, for instance, in implementing a priority system on terms. In the case where higher_valuedis simple arithmetic comparison of numbers,max_valuedcomputes the maximum number in a list.

The maximum-valued term of a nonempty list is the higher valued of the head of the list and the maximum valued term of the tail of the list. We will use a ternary max_valuedpredicate to capture this latter relationship between a list, a term, and the maximum-valued element in the entire bunch.

max_valued([Head|Tail], Max) :-max_valued(Tail, Head, Max).

The ternary predicate is easily implemented. If the list is empty, then the lone term is the highest valued. Otherwise, we pick the highest valued of the head of the list, the lone term, and the tail of the list, by using the ternarymax_valuedpredicate recur-sively.

max_valued([], Term, Term).

max_valued([Head|Tail], Term, Max) :-higher_valued(Head, Term),

max_valued(Tail, Head, Max).

max_valued([Head|Tail], Term, Max) :-higher_valued(Term, Head),

max_valued(Tail, Term, Max).

The clauses in the definition of ternarymax_valuedare mutually exclusive. In par-ticular, the last two require different relative magnitudes ofHeadandTerm. How-ever, if the second clause is used and later failure causes backtracking, the third clause will then be tried. The complete recomputation of higher_valued(Term, Head)will be performed which, by virtue of the asymmetry of the notion “higher valued”, will fail. However, arbitrary computation may have to be performed before this mutual exclusivity of the two clauses is manifested, because the computation of higher_valued(Term, Head)may be arbitrarily complex.

We can increase the efficiency ofmax_valuedby making this exclusivity explicit so that if the second clause is chosen, the third clause will never be backtracked into.

The following redefinition suffices:

max_valued([Head|Tail], Max) :-max_valued(Tail, Head, Max).

max_valued([], Term, Term).

max_valued([Head|Tail], Term, Max) :-higher_valued(Head, Term),

!,

max_valued(Tail, Head, Max).

max_valued([Head|Tail], Term, Max) :-higher_valued(Term, Head),

max_valued(Tail, Term, Max).

In this version of the program, as soon as we have ascertained thatHeadis higher than Termin value, we will eliminate the possibility of using the third clause, since we know that its first literal is doomed to failure anyway.

This cut maintains the semantics of the program only for certain modes of exe-cution of the program. In particular, if the mode ismax_valued(-,?), then the cut version may return fewer solutions than the uncut. However, use ofmax_valuedin this way will in any case generate instantiation errors if the arithmetic operators are used withinhigher_valued.

In summary, using cuts in this way without changing the meaning of the program can in some cases improve performance significantly. Nonetheless, this trick (and trick it is) should only be used when necessary, not when possible.

A second use of cuts is for eliminating redundancy in a program. Consider the alternateshufflepredicate defined by the following clauses:

shuffle(A, [], A).

shuffle([], B, B).

shuffle([A|RestA], B, [A|Shuffled]) :-shuffle(RestA, B, Shuffled).

shuffle(A, [B|RestB], [B|Shuffled]) :-shuffle(A, RestB, Shuffled).

Thisshuffleprogram correctly implements the shuffle relation. However, the predi-cate allows redundant solutions; we can see this by backtracking through the solutions it allows.

?- shuffle([a,b],[1],Shuffled).

Shuffled = [a,b,1] ; Shuffled = [a,b,1] ; Shuffled = [a,b,1] ; Shuffled = [a,1,b] ; Shuffled = [a,1,b] ; Shuffled = [a,1,b] ; Shuffled = [1,a,b] ; Shuffled = [1,a,b] ; Shuffled = [1,a,b] ; Shuffled = [1,a,b] ; no

The problem is that if one of the lists is empty, the program has the choice either of using one of the first two clauses to immediately determine the answer, or of traversing the nonempty list using one of the last two clauses. In either case, the solution is the same. One way to fix the predicate is to guarantee that the clauses are mutually exclusive.

5.1. Metalogical Facilities

This digital edition of Prolog and Natural-Language Analysis is distributed at no charge for noncommercial use by Microtome Publishing.

119

shuffle([], [], []).

shuffle([A|RestA], B, [A|Shuffled]) :-shuffle(RestA, B, Shuffled).

shuffle(A, [B|RestB], [B|Shuffled]) :-shuffle(A, RestB, Shuffled).

However, this solution might be seen as inefficient, since in the case that one of the lists is empty, the other list is still entirely traversed. An alternative is to place cuts after the first two clauses so that if one of the lists is empty, the use of one of the first two clauses will cut away the possibility of using the later clauses to traverse the nonempty list.

shuffle(A, [], A) :- !.

shuffle([], B, B) :- !.

shuffle([A|RestA], B, [A|Shuffled]) :-shuffle(RestA, B, Shuffled).

shuffle(A, [B|RestB], [B|Shuffled]) :-shuffle(A, RestB, Shuffled).

As a matter of style, we prefer the nonredundant solution with no cuts to this final one. The example was introduced merely as an illustration of the general technique of removing redundancy.

The third use of cut we will discuss here is a technique for implementing condi-tionals. Unlike the previous two uses, in which the declarative interpretation of the programs was the same whether or not the cuts were inserted, this use of cut actually changes both the procedural and the declarative interpretations. Such uses of cut are often referred to as “red” cuts, to distinguish them from the less dangerous “green”

cuts we first discussed.

A conditional definition of a predicate p of the form “if condition then truecase else falsecase” can be represented in Prolog using cuts as follows:

p :- condition, !, truecase.

p :- falsecase.

If the condition holds, the cut will prevent backtracking into the falsecase. On the other hand, if the condition fails, the truecase will, of course, not be executed. Thus the cases are executed just according to the normal notion of the conditional.

As an application of such a conditional, consider the merge function introduced in Section 3.4.1.

merge(A, [], A).

merge([], B, B).

merge([A|RestAs], [B|RestBs], [A|Merged]) :-A < B,

merge(RestAs, [B|RestBs], Merged).

merge([A|RestAs], [B|RestBs], [B|Merged]) :-B =< A,

merge([A|RestAs], RestBs, Merged).

The last two clauses can be thought of as saying: “if A<B then pick A else pick B.”

The predicate can be reimplemented using cut to reflect this conditional:

merge(A, [], A).

merge([], B, B).

merge([A|RestAs], [B|RestBs], [A|Merged]) :-A < B,

!,

merge(RestAs, [B|RestBs], Merged).

merge([A|RestAs], [B|RestBs], [B|Merged]) :-merge([A|RestAs], RestBs, Merged).

Certain versions of Prolog include a notation for conditionals which generalizes those built with cuts in this way. The goal

condition -> truecase ; falsecase

is used for this purpose. Use of the explicit conditional is preferred to implicit condi-tionals built with cut. With this notation, themergeexample can be rewritten as

merge(A, [], A).

merge([], B, B).

merge([A|RestAs], [B|RestBs], [C|Merged]) :-A < B

-> (merge(RestAs, [B|RestBs], Merged), C = A)

; (merge([A|RestAs], RestBs, Merged), C = B).

Note the use of the=operator defined in Program 3.2.

Dans le document Prolog and Natural-Language Analysis D (Page 126-130)