• Aucun résultat trouvé

Amalgamating Language and Metalanguage in Logic Programming

Meta-Interpretation

8.3 Amalgamating Language and Metalanguage in Logic Programming

As mentioned above, the introduction of a meta-interpretation layer enables us to reason explicitly about programs and their execution. In logic programming terms, it enables one to reason explicitly about theories (collections of clauses) and about the inference mechanism used to make derivations from these theories. We expressed concern, however, about the lack of control when we had an implicit meta-interpreter and an undisciplined approach to manipulating it. This can be overcome by making an interpreter explicit, but explicit in a limited way. Only those elements of the interpreter that we want to reason about are made explicit, while other elements remain implicit. For example, if we want a program that adds or deletes clauses from the set of clauses but does not make any changes to the inference mechanism, we can write an interpreter with an explicit representation of the clauses, but the inference mechanism remaining implicit and inviolate. In doing so, we limit the amount of damage caused by work at the metalevel. As mentioned above, the metalevel and object-level are in fact almagamated by giving the programmer control of both. But if the communication between the two is limited as much as possible in the program it is good software engineering principle, akin to other ways of dividing up programs into modules and limiting and making explicit the communication between the modules.

Bowen and Kowalski [1982] formalized discussion of metaprogramming in logic by introducing two rules that describe the interactions between the object-level and metalevel:

Pr |-M demo(A´,B´) A |-L B A |-LB Pr |-M demo(A´,B´)

These are referred to as reflection rules, the terminology coming from Feferman [1962] via Weyhrauch [1981]; it is related but not identical to the use of the term

reflection in introspective systems. Here |-M represents proof at the metalevel, while

|-Lrepresents proof at the object language level. So the first rule states that if it is possible to prove that demo(A´,B´) follows from the program Pr at the metalevel, then it can be inferred that B can be proved from A at the object level, while the second rule states the reverse. A´ is the representation of the object-level theory A at the metalevel and similarly B´ is the representation of the object-level expression B at the metalevel. This relationship is specified formally by a naming relationship [van Harmelen, 1992]. The naming relationship is a formalization of McCarthy’s packaging of program state into the “state vector” ξ in his meta-interpreter.

The classic “vanilla” meta-interpreter of Prolog is simpler than Bowen and Kowalski’s demo:

solve(true).

solve((A,B)) :- solve(A), solve(B).

solve(Goal) :- clause(Goal,Body), solve(Body).

because it makes no distinction between object-level and metalevel clauses. Here the object level and the metalevel language are identical, except that object-level predicate names are represented by metalevel function names. The blurring of object and metalevel in clause confuses even this. A particularly important point is that object-level variables are represented by metalevel variables.

We can remove the problem caused by Prolog’s clause by having the object-level program represented as an explicit value in the metalevel program, then we can define a demo which works as indicated by Bowen and Kowalski’s rules:

demo(Pr,[]).

demo(Pr,[A|B]) :- body(Pr,A,Body), demo(Pr,Body), demo(Pr,B).

The exact metalevel representation of object-level clauses will be further defined by body, which it can be assumed selects a clause from the object-level program Pr, whose head matches with A and returns in Body the body of that clause, importantly with variables re-named as necessary. The interpreter does require that an object-level clause body is represented by a metalevel list of metalevel representations of goals or by the empty list if it is true at the object-level.

What is crucial is that object-level control is represented implicitly by metalevel control. There is nothing in the interpreter that indicates that it is Prolog’s depth-first left-to-right with backtracking control. We could build a tower of such meta-interpreters and the control would remain in the underlying Prolog system (the inviolate hyper-interpreter, introduced earlier). Indeed, if it were running on top of GDC or some other non-Prolog logic language, it would inherit that language’s control mechanism. We can note therefore that contrary to McCarthy’s intentions with his Mini-Algol meta-interpreter, this metacircular interpreter does not fully define the logic language, indeed in amalgamating language with metalanguage it leaves it undefined.

The use of such a meta-interpreter comes when we do not wish to interfere with the control, but wish to add some extra processing alongside inheriting the control mechanism of the underlying language. The following meta-interpreter adds to the

“vanilla” interpreter a mechanism for counting the number of object-level goal reductions:

demo(Pr,[],N) :- N=0.

demo(Pr,[A|B],N)

:-body(Pr,A,Body), demo(Pr,Body,N1), demo(Pr,B,N2), NisN1+N2.

Another use might be an interpreter that simply prints each goal as it is reduced. This could thus be used as a simple tracer. A useful aspect of this interpreter is that in making the object-level program explicit we can handle program-altering primitives like assert and retract in a way which viewed from the metalevel does not violate the logical basis of the program:

demo(Pr,[]).

demo(Pr,[assert(C)|Rest]) :- !,

insert(C,Pr,Pr1), demo(Pr1,Rest).

demo(Pr,[retract(C)|Rest]) :- !,

delete(C,Pr,Pr1), demo(Pr1,Rest).

demo(Pr,[A|B]) :- body(Pr,A,Body), demo(Pr,Body), demo(Pr,B).

Some measure of the extent to which a primitive is merely extra-logical as opposed to metalogical may be gained by the ease with which it can be incorporated into a metacircular interpreter. Prolog’s cut performs badly on this front, since it is certainly not possible to represent an object-level cut by a metalevel cut; to implement it correctly would require a large amount of the underlying control to be made explicit in the meta-interpreter so that it can be manipulated. On the other hand, negation by failure is trivial to implement in the meta-interpreter; it requires just the addition of the following clause to the vanilla demo:

demo(Pr,[not(G)|Rest]) :- not(demo(Pr,[G])), demo(Pr,Rest).

The version of demo suggested by Bowen and Kowalski enables greater control to be exercised at the metalevel over the object level:

demo(Prog, Goals) :- empty(Goals).

demo(Prog, Goals)

:-select(Goals, Goal, Rest), member(Clause, Prog),

rename(Clause, Goals, VariantClause), parts(VariantClause, Head, Body), match(Head, Goal, Substitution), join(Body, Rest, NewGoals1),

apply(Substitution, NewGoals1, NewGoals), demo(Prog, NewGoals).

Here the selection of the particular object-level goal to reduce is determined by the metalevel procedure select and match determines the sort of matching of goal against clause head. It would be possible for select and join which adds the new goals to the existing waiting goals to be written so that the control regime provided at object-level is not Prolog’s depth-first left-to-right, but something else. For example, if join joined the new goals to the rear of the rest of the goals and select chose the first goal from the front, the result would be breadth-first expansion of the search tree.

Although match could be written as GDC style input matching rather than full unification, this interpreter still inherits Prolog’s underlying assumption of sequential execution. Note for example, it is assumed that a complete set of goals to be executed is passed sequentially from each goal reduction, with Prolog’s global substitution on unification assumed.