Declarativa Declarativa
 

 

"untiless" UNLP meta-interpreters

Miguel Calejo

Technical note draft, DI/UNL, 23 January 1989

Introduction

This note shows how to build UNLP [Porto Pereira Trindade 87] meta-interpreters. Previous interpreters made use of until or unless primitives, not defined in "pure" UNLP themselves. These primitives have been added to UNLP essentially to implement pure UNLP interpreters. The new interpreters to be presented avoid the need for them by:

a,b) using different representations for predicate definitions;

c,d) in a more tortuous way, using all and lazy all metapredicates.

We start by recalling the traditional interpreter, and then move on to an interpreter in two versions, using two different new predicate definition representations. Then we restart with the old interpreter, and instead reimplement until (and hence unless) in two different ways, using the standard (eager) all and a lazy all.

The traditional UNLP interpreter, T

For your convenience, next is a "traditional" minimal interpreter, due to Porto and Trindade. We're not considering the extensions to accomodate Prolog clauses with arbitrary cuts (i.e, the "conditional and" and "conditional or" body goals), inspired by Luís Moniz Pereira, or system predicates. The interested reader can check the cited reference.
(1) ti(true) !.(2) ti((G1,G2)) <-> ti(G1), ti(G2).(3) ti(G) <-> (rule(G,B), ti_cond(B,NB)) until iff_body(B), ti(NB).
(4) ti_cond(iff(C,B),B) <-> ti(C).(5) ti_cond(if(B),B) !.
(6) iff_body(iff(_,_)) !.

Clauses are accessed through the meta-predicate rule(Head,B). If the clause has an exclusive implication then B will be iff(Condition,Body), otherwise if(Body). until provides the magic of cuts within disjunctions. The Prolog definitions for until and unless follow:G until C :- G, (C, !; true).G unless C :- G, (C, !, fail; true).

The interpreter A

until in clause (3) is used to avoid producing more solutions to its conjunction goal argument when a committment occurs. Although UNLP forces committments to be explicit in the clause structure, the above interpreter does not detach the fetching of that information from the nondeterminism of the rule call. Consequently, the "committment character" of the current object clause used can not be explicit at the level of clause (3), and a "dynamic " committment must be used instead.

Instead of rule/2 we use predicate_definition/2. For each predicate P in the program there is a fact predicate_definition(P,Rule_sequence)

where Rule_sequence encodes the sequence of clauses constituting the predicate definition for P, and P is a most general instance of the predicate literal sharing no variables with the Rule_sequence term.

We can now rewrite clauses (3-6) above, obtaining the new (meta) interpreter:
(3') i(G) <-> predicate_definition(G,Rules), i_body(Rules,G).
(4') i_body([rule(G,iff(C,B))|_],G) <-> i(B) <= i(C).(5') i_body([rule(G,if(B))|_],G) <- i(B).(6') i_body([_|Rules],G) <-> i_body(Rules,G).

Having explicitly separated the fetching of clauses from their use in the meta-interpreter, object level committments can now correspond directly to ordinary meta-level committments (clauses 4' and 5'). Shallow backtracking at the object level maps into shallow backtracking and recursion at the meta-level.

The interpreter B

Another possibility is using a "next_clause" primitive, in the vein of [Parker88]. Let's consider that we have 3 primitives for accessing rules in a predicate definition: first_ref(G,Ref)

returns a reference for the first rule in the definition for the predicate of goal G. Does not produce bindings in G. instance(Rule,Ref)

returns the "rule" term for the rule referenced by Ref. next_ref(Ref1,Ref2)

returns the reference Ref2 for the next rule after the one with reference Ref1. Of course, this primitive could have the goal as an extra argument, in order to take advantage of indexing.

The interpreter now becomes:
(3") i(G) <-> first_ref(G,R1), instance(Rule,R1), i_body(Rule,G,R1).(4") i_body(rule(G,iff(C,B)),G,_) <-> i(B) <= i(C).(5") i_body(rule(G,if(B)),G,_) <- i(B).(6") i_body(_,G,R1) <- next_ref(R1,R2), instance(Rule,R2), i_body(Rule,G,R2).

Implementationwise: T vs A vs B

Using "predicate_definition" instead of "rule" (A) is inherently worse, at least in a structure-copying implementation, since all rules have to be fetched even if they're not needed. It is also worse than the interpreter B using the "next_clause" primitives, as these are based on cheap pointer chasing and the interpreter fetches one rule at a time.

The winner in efficiency will possibly be the older interpreter T, since interpreter B unifies the goal G with the rule head only after it fetches it. This seems worse because a unification that fails will do so a posteriori, on the next iteration of i_body: the unification is being made explicitly (in clauses (4",5")), while the older interpreter T makes it implicitly in the rule call. The interpreter B should cause a full "rule" term to be built in the Prolog heap, while interpreter T can benefit from the interleaving of that construction with the unification of the goal with the rule head. This is hardly surprising: the extra expressivity available (making unification explicit) comes with a price.

Spacewise, both interpreter A and interpreter B required an additional recursion for each clause fetched. Therefore, A does not recover space on backtracking as in T, and for B to recover it a Prolog implementation with garbage collection is needed.

As stated above, the next_ref implementation might take indexing into account by having G as an extra argument, but it can't be allowed to bind its variables if it is to be used explicitly in the interpreter.

Both of the new interpreters above can be obviously improved by using first argument indexing in themselves, but that shouldn't affect the previous comparisons.

The interpreter C

Another possibility for making interpreters without until/unless is obviously to get rid of these in general. Following a suggestion by António Porto, we now present new definitions of the primitives until and unless which use an all solutions metapredicate, instead of resorting to Prolog cuts within disjunctions (cf. with the previous definitions). With these new definitions we can use the good old interpreter T (and call the combination "interpreter C").
G until C <-> all(S,G,Solutions), until(Solutions,G,C).
until([G|_],G,C) ! <= C.until([G|_],G,_).until([_|Solutions],G,C) <-> until(Solutions,G,C).
G unless C <-> all(GG,G,Solutions), unless(Solutions,G,C).
unless([G|_],G,C) # <= C.unless([G|_],G,_).unless([_|Solutions],G,C) <-> unless(Solutions,G,C).

In the interpreters above, getting all the clauses in a predicate definition at once (A) or while iterating forward (B) allowed us to replace the until call at the interpreter level by an UNLP if-then-else. To implement until/unless in the general case as above we collect all solutions to G and test the condition C afterwards, needing only UNLP's if-then-elses as well. Like in the A interpreter case this implementation is inherently less efficient, since all solutions need to be computed a priori.

The interpreter D

On the other hand, using a lazy all primitive (for example, [M.Pereira Nasr 84]) would have the benefits the "next_clause" primitives brought to the B interpreter.

Consider the primitives: first_solution(G,S,R)

Computes the first solution S for G, without producing bindings in G and returning a reference R denoting the state of that computation (after S is computed) next_solution(R1,S,R2)

Takes the state of an ongoing computation of some top goal G, denoted by reference R1, and computes the next solution S to G, obtaining a new state of that computation, denoted by reference R2. Fails if there are no more solutions to G.

The implementation of these primitives must rely on some sort of parallel computation, with its own backtracking, such as a coroutine or a remote process, for example.

until can now be defined as follows (the reader may check that unless can be implemented similarly):
G until C <-> first_solution(G,S,R), until(G,S,C,R).
until(G,G,C,_) ! <= C.until(G,G,_,_).until(G,S,C,R1) <-> next_solution(R1,S,R2), until(G,S,C,R2).

We call "D" to the interpreter T using this until definition.

Conclusion

All this seems to be rather academic, as the new interpreters are either slightly or much more inefficient than the traditional one. The extra control gained in interpreter B seems hardly useful at all. Implementing until/unless with lazy all seems a curious and useless enterprise, given the implicit implementation costs…

The motivation has been instead to "purify" UNLP a little further, attempting to make it independent of Prolog cuts.

References

António Porto, L.M.Pereira and Luís Trindade, "UNLP reference manual", UNL ALPES report 1987

D. Stott Parker, Thomas W.Page, Jr. and Richard Muntz, "Improving Clause Access in Prolog",, UCLA CS Dept., 1988

Luís Moniz Pereira and Roger Nasr, "Delta-Prolog" (??), Proceedings of the Conference on Fifth Generation Computer Systems 1984, ICOT, Tokyo.

Acknowledgements

To António Porto, Luís Moniz Pereira and Luís Trindade for relevant suggestions and criticisms.


 Declarativa - Serviços de Informática, Lda.
  www.declarativa.com, info@declarativa.com  fax: +351-22-610 9574  tel: +351-22-610 9516
*Sede social (correio):
R. Cerca 88 4150-200 Porto Portugal
 *Centro de Desenvolvimento:
UPTEC - Parque de Ciência e Tecnologia da Universidade do Porto
Rua Actor Ferreira da Silva 100 4200-298 Porto Portugal