translates each Prolog predicate into a Gofer function such that both the ...... of the clause used for resolution and the unifying substitution is applied to the new ...
Shallow Embedding of Prolog Programs Thesis submitted for the transfer to full DPhil status
Silvija Seres Oxford University Computing Laboratory The Wolfson Building Parks Road Oxford April 12, 1999
Abstract C.A.R. Hoare's Uni ed Theory of Programming gives a single framework for describing the algebraic semantics of dierent programming paradigms. The work presented in this thesis is aimed towards incorporating the lacking parts of logic programming paradigm into this Uni ed Theory. As a rst step in my algebraic study of logic programming, I propose a shallow embedding of logic programs into Gofer programs. This embedding translates each Prolog predicate into a Gofer function such that both the declarative and the procedural reading of the Prolog predicate are preserved. In the standard approach to mapping logic programs to functional ones the declarative reading is lost. The shallow embedding computes by means of operations on lazy lists. The state of each step in computation is passed on as a list of substitutions, and all the implicit logic operators in Prolog are replaced by explicit Gofer operators on lists. I express a set of algebraic laws for these operators and discuss how well they capture the algebraic semantics of Prolog. I conclude with examples showing how the proposed embedding is applied for algebraic reasoning about Prolog programs.
Contents 1 Introduction
3
2 Comparing Functional and Logic Programming
8
2.1 Example of a Logic and a Functional Program . . . . . . . . 8 2.2 Outline of the Dierences . . . . . . . . . . . . . . . . . . . . 10
3 The Shallow Embedding 3.1 3.2 3.3 3.4
The Declarative and Procedural Semantics of Prolog Example of Shallow Embedding . . . . . . . . . . . . Implementation of Types . . . . . . . . . . . . . . . Implementation of Operators . . . . . . . . . . . . .
4 Algebraic Properties of the Operators 4.1 4.2 4.3 4.4
Equality and Healthiness of Predicates Uni cation and Substitution . . . . . . Basic Combinators . . . . . . . . . . . Variable Introduction . . . . . . . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
13 13 16 18 19
23 23 26 26 30
5 Reasoning about Predicates
32
6 Conclusions, and DPhil Proposal
40
5.1 Example with Adjacent Elements of the List . . . . . . . . . 32 5.2 Example with Reversing of a List . . . . . . . . . . . . . . . . 35
1
6.1 Related Work . . . . . . . . . . . . . . . . . . . . . 6.2 Further Work . . . . . . . . . . . . . . . . . . . . . 6.2.1 Set-Equality in the Shallow Embedding . . 6.2.2 The Implementation of Full Prolog . . . . . 6.2.3 Exploring Dierent Computation Strategies 6.2.4 Higher Order Programming . . . . . . . . . 6.3 Conclusions . . . . . . . . . . . . . . . . . . . . . .
A Code for the Operators A.1 A.2 A.3 A.4 A.5
General Types . . . . . . . . Higher Order Predicates . . . Integer and List Modelling . . Auxiliary Functions . . . . . Substitution and Uni cation .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
40 41 42 42 44 50 51
53 53 53 54 55 56
B Some Examples in the Shallow Embedding
57
C Rewriting for Examples from Chapter 5
62
B.1 Computations with append . . . . . . . . . . . . . . . . . . . 57 B.2 A Family Database Example . . . . . . . . . . . . . . . . . . 59 B.2.1 Test run . . . . . . . . . . . . . . . . . . . . . . . . . . 61
C.1 The Adjacent Elements Example . . . . . . . . . . . . . . . . 62 C.2 The Reversing Example . . . . . . . . . . . . . . . . . . . . . 66
2
Chapter 1
Introduction Programming is a wide and multi-directional area, built up from many paradigms. Among the most known are imperative, functional, logic, and parallel programming. Each of these paradigms has given rise to dierent mathematical ways of expressing the meaning, or the semantics, of the programs. Some examples are Hoare triples for imperative programming, -calculus for functional programming, Lloyd's-style semantics for logic programming and CSP (or BSP) for concurrent systems. Many of these interpretations are not well suited for describing other paradigms than their \native" one. Constructing a single mathematical framework for the semantics of programs in dierent paradigms gives both a uni ed picture of the whole eld and a better understanding of each of its fragments. There are three common semantical models which can be used across the paradigm boundaries. Denotational semantics describes the meanings of programs in terms of mathematical functions on domains, operational semantics considers the meaning to be the evaluation of programs, while in the algebraic semantics the meanings of programs arise from algebraic laws expressing equivalences between programs. Domain theory is used to give a denotational semantics to programs, and can be considered as a unifying theory. The diculties associated with interpreting the meaning of programs by domain theory are made apparent, for example, in the task of checking whether a certain text is a valid program. Domain theory requires reasoning on a semantical level to resolve this question. The alternative would be to devise a more syntactical approach, allowing the use of algebraic laws and relationships in reasoning about program semantics. Unifying Theories of Programming does this by de ning 3
sets of algebraic laws describing healthiness conditions that texts have to satisfy to be valid programs, and additional laws determining other properties of the programs within the dierent paradigms. This prject is not primarily driven by practical motives; the goal is not to construct a \swissknife" of programming languages, but to give a comprehensive mathematical framework in which the nature of all the languages can be described. Logic programming in its concurrent form has been partially incorporated by the Unifying Theories in [HJ98]. There are, however, other important questions about logic programming that have not been covered in this book, for example, the semantics of parameter passing, of substitution and of uni cation. Filling out the gaps in the Unifying Theories treatment of the logic programming motivates my research. A useful step in this direction is to construct an embedding of a logic programming language onto a functional programming language. A functional language can be seen as a system for expressing algebraic equalities, and an execution of a program in such a language is a (directed) form of equational reasoning. There is a standard way to map logic programs onto functional ones. In the standard approach the chosen functional language is used to implement an interpreter for program texts written in the logic language. In this case, for example, a logical program becomes an atomic object when interpreted as an input to a functional program, so that it is impossible to reason whether important properties of the original program are maintained in the functional model, and the comparison of two logic programs in such model can only be done by comparing their results. Logic program Queries
Interpreter 11 00 00 11 00 11 00 11 00 11 00 11
Output
11 00 00 11 00 11 00 11 00 11 00 11
We call this approach deep embedding of logic programming in functional programming as opposed to the shallow embedding that I propose. The former corresponds to an interpretation, and the latter to a translation, of a logical program to a functional one. The functions in the shallow embedding do not need a designated program or an interpreter to execute, they run in the basic functional system augmented with an implementation of a few operators that correspond to the primitives of the logic program. 4
Logic program
Function definitions
Queries
Function calls
Library functions
11 00 00 11 00 11 00 11 00 11 00 11 1111111111 0000000000 1111111111 0000000000 00 11 00 11 00 11 00 11 00 11 00 11
00 11 00 11 00 11 0000000000 1111111111 00 11 00 11 00 11
Output
In the shallow embedding a logic program is represented by a set of functions that have the same structure and declarative reading as the original predicates. The functional embedding is at the same abstraction level as the logic source program. All the primitives of a logic program are explicit operators in the functional model so an algebraic analysis or manipulation of the rst program can be done by the use of the algebraic laws for the functional operators of the second program. In this thesis the choice of the embedded source language is pure Prolog, for simplicity reasons, and so far I only implement the mapping for pure Prolog without cut. The choice of the target functional language is guided by the ease of implementation of the logic programming features that I want to cover. Uni cation and substitution can be easily implemented in any functional language. Resolution can be simulated by function calls which take as input the previously computed substitutions, the \store" of the logic program. Each consequent computation is done relative to this store and returns a list of possible updates to the store. This list can be in nite in some cases. Lazy evaluation on lists can be used in order to model such in nite answers in the functional program, and Gofer is a simple typed lazy functional language well suited for programming with such lists. The remaining characteristic features of logic programming that I focus on are search, backtracking and the exibility of parameter passing. Search and backtracking in logic programming are based on the building of the search tree, and in the functional model this tree is mapped onto the list of computed substitutions. The lists I implement correspond to the SLD-tree and are traversed in a depth- rst order, but the use of other search strategies is a subject of my further research. The implicit and and or operators in Prolog that direct the building of the SLD-tree, are implemented as list operations in Gofer. Parameter passing is performed through uni cation and Gofer's explicit lambda expressions which bind the parameters to the expression consisting of function calls that implement the body of the Prolog clause. One important application of the shallow embedding is reasoning about program transformation of logic programs. For example, the result of optimis5
ing a program is often a program which looks quite dierent, but eventually arrives to the same results as the previous version. Mathematically establishing equivalence of two logic programs can prove dicult, since it involves reasoning about their least Herbrand models, which cannot be done on the syntactical level (it involves the \closed world assumption" at some point in the proof). Employing the shallow embedding allows us to use structural induction and function rewriting to establish the equivalence of the functions corresponding to the two logic programs. This is performed by rst reducing both functions to the same recursive form, and then appealing to the uniqueness of the solution of such recursive equations. The diagram below illustrates the process. Prolog program p1 o
Prolog program p2 /
shallow embedding
shallow embedding
Gofer function f1 o
rewriting
/
Gofer function f2
The dierence between these two approaches for proving program equivalence corresponds to the dierence between mathematical deduction and equational reasoning. Deduction involves proving that the two programs are equivalent in their consequences, while equational reasoning is a much simpler task of following a chain of mathematical equations. Shallow embedding can similarly be used for general reasoning about logic programs. The shallow embedding the simplest possible formal mechanized declarative semantics for Prolog. Nevertheless, it is optimal for an algebraic study of Prolog and for equational reasoning about Prolog programs. In chapter 6 I suggest that it is also well suited for experimenting with dierent search strategies and higher-order prodeicates in Prolog because of the ease of implementing these in the shallow embedding. Here is a brief outline of the thesis. Chapter 2 compares the logic and functional programming paradigms, stressing their dierences. Chapter 3 describes the declarative and recursive semantics of a Prolog program, and shows how these lead to the construction of the shallow embedding. I also discuss there the implementation of the primitive operators 6
of the embedding, and state and prove certain algebraic properties of these operators. A study of two examples in chapter 5 shows how the algebraic properties of the embedding operators are used for proving statements about the functions corresponding to Prolog program. Chapter 6 contains a summary of the thesis, a description of related work and some ideas for further research. I include a short summary of the material I already have read and reading list that I intend to cover. The code for my implementation for the shallow embedding, an example of shallow embedding of a Prolog program and the full text of some of the proofs from chapter 5 can be found in Appendix.
7
Chapter 2
Comparing Functional and Logic Programming The functional and the logic programming paradigms have much in common. Both of them were developed in order to overcome the mathematical shortcomings of the von Neumann model of computation. Both imply incremental languages designed primarily for symbolic data processing. Both paradigms are declarative in the sense that they express what has to be computed instead of how it should be computed, which in turn leads to elegant and simple program code. Both are based on dynamic storage and garbage collection. Neither of them (in their pure form) incorporate the machine-oriented concepts of assignments and references. Since both these paradigms are mathematically well-founded they allow greater possibility of formal manipulation and (claimed) ease of parallel evaluation. However, there are important fundamental and stylistic dierences between the two paradigms. In this chapter I outline some of these dierences, although I in no way attempt to give a complete and precise overview of all the properties of all the languages belonging to these paradigms.
2.1 Example of a Logic and a Functional Program Let me introduce a simple example in order to illustrate the dierences between the two paradigms. Suppose we need to de ne a function append for concatenating two lists. I use Prolog notation as an example of a logic programming language and Gofer as a functional one. 8
In logic programming a program consists of Horn clauses which are of the form A:-B1,...,Bn, where A is called the head of the clause and B1 ,...,Bn the body. The head of the clause is the procedure's entry point and it displays the possible form of the arguments to the predicate. The body of the clause consists of a number of goals (procedure calls) which impose conditions for the head to be true. For example, let the relation append(A,B,C) mean that A appended with B results in the list C. Then append can be de ned by the two clauses: append([],X,X). append([X|A],B,[X|C]):- append(A,B,C).
The rst clause states that appending an empty list to any list does not change the latter. The second clause is a backwards implication saying that if append holds for (A,B,C) then it will still be true after adding the same element at the front of A and C. To execute a logic program one has to give it an initial goal of the form C1 ,...,Ck ?. The program clauses are interpreted as procedure de nitions, and each Cj in a goal is interpreted as a procedure call. A step of computation consists in matching a procedure call from the current goal with a head of one of the program clauses. This is done by a process called uni cation. In logic programming uni cation is the only mechanism for parameter passing, data selection and data construction. Returning to our example, append can be used to check whether concatenation of the two input lists yields the third one. In this case the output of the program takes the form of a simple yes or no. As an example, we may submit a query append([1,2],[3],[1,2,3])?, which yields yes as the result. On the other hand, a query to a logic program may also contain variables; these are assumed to be existentially quanti ed. In this case computing the logic program corresponds to a constructive proof of the existentially quanti ed formulae deriving from the query. The computation of logic programs can declaratively be explained as a proof by refutation; this means that to prove, for example, append([1,2],Y,[1,2,3])? we assume that there exists a variable Y which makes append true but results in an inconsistent state of the logic program. We try to construct such a Y in all possible ways, and for each answer that fails to give an contradiction we have a candidate answer for the query. The output to append([1,2],Y,[1,2,3])? then will consist of only one list, Y=[3], but in some cases the output may be in 9
nite. For example, if the query is append(X,[1,2],Y)? the list of answer substitutions would be (X=[],Y=[1,2]), (X=[ 0],X=[ 0,1,2]),: : : A functional language de nition of append resembles the logic one super cially. For example, here it is coded in Gofer: append [] x = x append (x:a) b = x:(append a b)
Predicate de nitions in the logic programming map to function de nitions in functional programming. Functions have the arity one less than the corresponding predicates, because one of the arguments of a predicate stands for the result of the function. Each pattern-matching case of a function de nition corresponds to one clause. Evaluating a function proceeds by rewriting the de nition after substituting the input for the arguments. This process results in a single thread of computation leading to the answer. Any unresolved arguments will stop the calculation, so that in functional programming there is no exact counterpart to the computation of append([1,2],Y,[1,2,3]).
2.2 Outline of the Dierences Here is the summary of the important points in which the logic programming and the functional programming dier.
Computational model. Functional languages stem from Church's lambda
calculus and recursion equation systems, whereas logic programming languages are based on a procedural interpretation of the rst order predicate calculus. Computation in functional programming is a sequence of reductions on terms, computation in a logic program is a proof by refutation. To execute a program in a functional language is to reduce an expression until it contains only constants or constructor functions. To execute a logic program is to submit a goal clause, which the system attempts to satisfy as a predicate, listing all values of the variables that make the clause true without contradicting the other clauses of the program. Notation. Functional programming assumes implicitly the de nitions of many auxiliary variables that have to be explicitly mentioned in logic programming. Notably, the output of a function, described by the 10
right-hand side of its de nition, remains unnamed in a functional language, while in a logic language each component of the output has to be introduced in the clause head. The intermediate variables arising from function composition are also unnamed in functional programming, but have to be explicitly de ned in logic programming. For example, in (f g)x = f (g(x)) one needs to introduce y = g(x) when writing in a logic programming language. Determinism. Functional programs are deterministic. In other words, for any input value a unique output is computed. As a corollary of some Church-Rosser properties, one can show that no backtracking is necessary for evaluating a functional program. Logic programming languages are intrinsically nondeterministic in the sense that any query may give rise to several solutions. Evaluating a logic program consists in a search process. At any point of the evaluation several search paths to proving a subpart of the goal may be available, but not all of them, if any, lead to success. There exist complete search strategies, but most logic program interpreters implement incomplete strategies for eciency reasons. For example, depth- rst backtracking can choose to explore a non-terminating part of the search space before exploring a terminating part. Therefore a logic program may fail to nd solutions even when they exist according to the declarative reading of the program. Multi-mode use of relations. Logic programming uses relational notation for clauses and resolution as an evaluation mechanism. This makes it possible for a logic program not to make a commitment as to which variables in a relation are to be considered as inputs and which variables are to be considered as outputs. A relation once de ned can be used in many modes. Functional programs are committed as to what they regard as inputs and what they regard as outputs, so one logic program can represent many functional ones (although in practice problems of nontermination restrict this exibility). Non-ground outputs. Logic programming languages may output a result which is not completely ground, i.e., it may involve logical variables. These data structures may be seen as skeletons for all ground clauses/terms that can be pattern-matched and they lead to many abstract and ecient logic programs. Functional languages allow only constants and constructor functions to appear in the output. On the other hand, functional languages commonly treat functions as rst
11
class objects so they can be passed as parameters and returned as values. There are some logic programming languages that allow this (notably -Prolog) but traditionally they are rst order languages. In addition, many functional languages allow polymorphic typing, with strong but exible type checking, while logic programming languages are (traditionally) untyped. Despite the dissimilarities in computational model and semantics of the two paradigms, they are close enough to allow us to translate all the central features of logic programming into a functional model on the same level of abstraction. The features in focus are uni cation and resolution, search and backtracking, and the parameter passing exibility of logic programs. The description of such a translation together with a library of functions for the computation of the embedded predicate, forms the topic of the next chapter.
12
Chapter 3
The Shallow Embedding Because of the dierences described in outline in the previous chapter, one cannot hope to achieve the perfect mapping from the logic programming paradigm to the functional one. The features of logic programming that I want to preserve in the embedding are the structure and the granularity of the program, the properties of parameter passing and the backtracking mechanism in search. This chapter is concerned with the embedding I propose, implemented in practice for Prolog to Gofer translation. The chapter starts with the discussion of Prolog semantics necessary for understanding the data-types involved in the embedding and in the implementation of the operators. I give an example and proceed to describe the actual implementation. As short series of experiments of running Prolog programs with comparative tuning to the shallow embedding can be found in appendix B; a glance at those makes the embedding described in this chapter more interesting and convincing.
3.1 The Declarative and Procedural Semantics of Prolog There are two alternative ways of understanding the semantics of Prolog. The procedural semantics is the more traditional one, because it describes execution as a sequence of states of a program, much like the standard operational semantics of imperative programming. The declarative semantics is the reading that provides a way of understanding a Prolog program as a set of descriptive statements about a problem. 13
Prolog inherits its declarative semantics from logic. This semantics de nes recursively the set of terms which are asserted to be true according to a program: a term is true if it is the head of some clause instance and each of the goals (if any) of that clause instance is true. In this way the declarative semantics gives one some understanding of a Prolog program without looking into the details of how it is executed. In the declarative semantics of Prolog the clauses of a program P are its axioms. One step of computation consists in nding an instance of an answer which is a consequence of the program's set of axioms. For example, if a query append([1,2],Y,X) is being computed, Prolog tries to nd a substitution such that the instance append([1,2],Y,X) follows from the programs set of axioms. Each successful computation yields such a substitution (the most general one for the given combination), and can be viewed as a proof of append([1,2],Y,X) derived from the axioms. The procedural semantics describes the way a goal is executed. The object of the execution is to produce true instances of the goal. To execute a goal, the system searches for some clause whose head matches or uni es with the goal. The uni cation process nds the most general common instance of the two terms, which is unique if it exists. If a match is found, the matching clause instance is then activated by executing, in some order, each of the goals of its body (if any). If at any time the system fails to nd a match for a goal, it backtracks, i.e. it rejects the most recently activated clause, undoing any substitutions made by the match with the head of the clause. Then it reconsiders the goal which activated the rejected clause, and tries to nd a subsequent clause which also matches the goal. The computation described above is called resolution. These computation steps are repeated until the empty clause, which is always true, is produced, or until a state is reached where no uni cation is possible. The system either backtracks to choose another combination of a goal and a clause, or terminates with failure. The ordering of clauses in a program, and of goals in a clause, are irrelevant as far as the declarative semantics is concerned, but they constitute crucial control information for the procedural semantics of the program. The description above corresponds to many procedural interpretations of a logic program, because there may be many literals in a goal and many clauses uni able with each literal. All the dierent ways of combining the clauses of a program and the order in which the literals in one clause are chosen for resolution form a large and complex search space, but this search space can 14
be greatly reduced by using a predetermined strategy. One particular strategy for resolution, called SLD-resolution, takes in every step the goal that was produced in the previous step and a clause taken directly from a program, and resolves the literals from the goal in a strict left-to-right order. A result stated below shows that the strategy chosen by SLD-resolution is as powerful as any other. The choice that remains during SLD-resolution is which program clause to choose when more than one is applicable to the current goal, and this is the choice that makes Prolog programs non-deterministic. The possible choices can be shown in form of a search tree, with a root in the original goal and each node branching in the possible derived goals from it by using various clauses in a single step of SLD-resolution. Some branches may end successfully, some may be in nite and some may end in failure. H1:-B1,B2,B3. H1:-B4,B5. H1,H2,H3
B1,B2,B3,H2,H3
B4,B5,H2,H3
...
...
fail ...
The standard way to traverse the search-tree in Prolog is depth- rst traversal of the search tree. In other words Prolog chooses one child of the node (in the order that the clauses corresponding to these children appear in the program) and explores that child and all its descendants before considering any other children. This search strategy is easy and ecient to implement, but may get stuck on an in nite branch when there exist other successful branches, thus making Prolog incomplete and sensitive to the order of the clauses in the program. If a more fair search strategy is used, for example breadth- rst search, the the following result SLD-resolution holds. The correct answer is a declarative description of the desired output from a program and a goal, and the computed answer is its procedural counterpart, which is de ned using SLD-resolution. The SLD-resolution process is sound, in the sense that all computed answers correspond to one of the correct 15
answers described by the declarative semantics, and it is complete in the sense that every correct answer is an instance of a computed answer. The main motivation of the embedding I propose below is to preserve the procedural semantics of Prolog in a Gofer model. This can be done by a so called deep interpreter where both the input Prolog program and the computation described above are atomic from the users point of view. Such interpreting of Prolog programs obscures the connection between the declarative semantics of the input Prolog program and the built-in procedural semantics; the interpreter behaves like a black box. In the shallow embedding I describe in the next section I aim to map Prolog predicates to Gofer in a way that preserves both the structure and the behaviour of the original predicate.
3.2 Example of Shallow Embedding The following example illustrates a translation of a Prolog predicate append to a Gofer function. Predicate append is de ned as: append([],X,X). append([X|A],B,[X|C]):- append(A,B,C).
This set of clauses is translated to a Gofer function below, where eqn means equal, exists 1 (\ [x] -> yx) means there exists x such that yx, &&& means and and ||| means or. The translation steps are described after the function de nition: append(p,q,r) = (exists 1 (\ [x] -> eqn(p,nil) &&& eqn(q,x) &&& eqn(r,x))) ||| (exists 4 (\ [x,a,b,c] -> eqn(p,(cons x a)) &&& eqn(q,b) &&& eqn(r,(cons x c)) &&& append(a,b,c)))
To translate any Prolog predicate to a Gofer function, do as follows: de ne a Gofer function with the same name and arity as the Prolog predicate. De ne one |||-branch for each constituent clause. This corresponds to the implicit disjunction of the clauses in Prolog. For each clause, if the clause 16
contains variables, apply an exists operator followed by the number and the list of the variables in the respective |||-branch. This corresponds to the implicit existential quanti cation of the variables in the Prolog predicate. The listed variables are bound to the remaining part of the |||-branch with Gofer's explicit lambda-expression, and the (\ and the -> are Gofer syntax for de ning such lambda-expressions. For simplicity of translation keep the same variable names as in Prolog, only lowercased. It is unfortunate that extra variable names are used for the function parameters, but is necessary because Prolog uses uni cation where Gofer only has pattern matching. This could in future be hidden by means of an automatic translator. Each of the |||-branches is built in this manner: For each of the function parameters use the eqn operator to pass the parameter value. This operator performs uni cation (relative to an input substitution), and returns the satisfying substitution(s). Use the &&& operator to connect the eqn-calls. This operator passes the substitutions computed so far as an input to the next function, be it eqn or some other function. Translate all the literals in the body of the clause to function calls with the same function name, arity and same variable names, and insert each function call in the tail of the branch using &&&. The Gofer function append is constructed by making the declarative reading of the Prolog predicate explicit. Consequently, the computational behaviour of the two prove to be identical, as illustrated by examples in appendix B. However, the relation of the Gofer function and Prolog predicate extends beyond their structure, level of abstraction, and input/output equivalence. The procedural reading of the Prolog predicate is also preserved. The shallow embedding essentially allows the mapping of the computation of the Prolog program on lazy lists. It performs an embedding of the SLD-tree of a Prolog program into a structure of a list containing the substitutions for the Gofer function. More examples of the translation and the execution of the functions in the shallow embedding can be found in appendix B. The types and the meaning of the four operators of the embedding, |||, &&&, eqn, and exists, are described in detail in the next two sections.
17
3.3 Implementation of Types A computation of a logic program can be viewed as a sequence of operations on the \store" of the program, which is simply the sequence of computed uni ers. The result of a logic program is the composition of the uni ers from each step. The type of the function append is (Term,Term,Term) -> Predicate. In general, for all the functions in this embedding that are translations of Prolog predicates, the input type will be a Term tuple with the same number of elements as the predicate, and the return value will always be a function of type Predicate de ned as: type Predicate = Answer -> [Answer] type Answer = (Subst, Int)
The input to a Predicate is the current result of the interpretation so far. It is of the type Answer which is a pair consisting of the computed substitution and the number of variables created so far. This number is needed for the automatic generation of variable names performed by the exists function as explained later. All variable names bound to the input parameters of a clause must be fresh in order to guarantee that the computed answer is the most general result. The return value of a Predicate function is a list of computed answers. An empty list of answers means that the predicate is not satis able given the input substitution. A substitution is represented as a list of (variable, term) pairs. type Subst = [(Var, Term)]
Variables can be user de ned or automatically generated, and terms are trees with the following structure: data Term = Func Func [Term] | Var Var type Func = String data Var = Name String | Gen Int
Constants are functions with arity 0, in other words they are given empty argument lists. For example the Prolog list [a,b,c] can be represented in the embedding as Func "cons" [Func "a" [], Func "cons" [...]]. 18
In the examples I use the auxiliary functions cons, atom and nil for better readability: cons :: Term -> Term -> Term cons x a = Func "cons" [x,a] nil :: Term nil = atom "nil" atom :: String -> Term atom a = Func a []
With these functions the Prolog list [a,b] becomes: cons
a (cons b nil).
3.4 Implementation of Operators The implicit logic operators in Prolog are implemented as higher order functions in the shallow embedding. They act as connectives for functions representing predicates, in the sense that they combine the computed answers from the predicates. Since the answers are lists of substitutions the operators are implemented in terms of operations on lists. Gofer employs a lazy evaluation mechanism called the normal order reduction. The reduction order of a program is irrelevant as long as only strict functions are involved, but the Predicate functions in the shallow embedding are non-strict. They may not terminate, in the same manner as a Prolog query append([1,2],Y,X)? does not terminate. The use of lazy evaluation in the shallow embedding is crucial for computation of the in nite list of answers that this predicate returns. The shallow embedding of a Prolog predicate is a function consisting of one |||-branch for each clause of the predicate. In the declarative semantics of Prolog the clauses are treated similarly to disjunction, so in the shallow embedding the lists of answers from all the branches should be returned by the function. This is implemented by list concatenation. (|||) :: Predicate -> Predicate -> Predicate (p ||| q) x = (p x) ++ (q x)
19
This de nition implies that the |||-branches are evaluated from left to right as the branches in a Prolog search-tree would be. If a |||-branch is unsuccessful and returns an empty answer list, it corresponds to an unsuccessful branch of the search tree in Prolog and the backtracking is simulated by pursuing the next |||-branch. If the rst |||-branch returns an in nite list of answers the second branch will never be evaluated, just as with depth rst search in Prolog. Also, if the rst branch does not terminate, we never reach the second branch. When a literal in a goal in Prolog is resolved, it is replaced by the body of the clause used for resolution and the unifying substitution is applied to the new goal. The next literal is then resolved, and the resulting substitution is applied to the whole goal again etc. This passing of the resulting substitutions between the literals of a Prolog clause is implemented by the conjunction-like operator &&&. The higher order &&& operator starts with evaluating its rst input Predicate function. Since Gofer employs lazy evaluation, the function call returns the answers one by one and the &&& operator feeds them to its second Predicate argument, which computes new lists of answers for each input. Finally, in order to preserve the type, all the resulting answer lists need to be concatenated into a single list. (&&&) :: Predicate -> Predicate -> Predicate (p &&& q) x = concat (map q (p x))
If either of the predicates returns an empty list, the behaviour is still correct and corresponds to backtracking. If any of the predicates return an in nite list the result is an in nite list of answers, the same way as in Prolog. Even if the rst predicate results in an in nite list the system manages to return the answers one by one, thanks to the lazy evaluation of the answer lists. The boolean constants TRUE and FALSE are useful for de nitions of algebraic laws for the operators. They are implemented by functions yes and no: no :: Predicate no x = [] yes :: Predicate yes x = [x]
20
In the next chapter the algebraic properties of the functions are stated and proved. Among others I show that the function yes is an identity element for the &&& operator (i.e., yes &&& p = p) and that no is an identity element for ||| (i.e., no ||| p = p). Uni cation, and therefore also parameter passing in Prolog style, is implemented by eqn: eqn :: (Term,Term) -> Predicate eqn (t1,t2) (s,n) = case (unify s (t1,t2)) of Just u -> [(u,n)] Nothing -> []
Here we assume a function unify of type Subst -> (Term,Term) -> Maybe Subst that does uni cation relative to the input substitution s. Please refer to appendix A for its full implementation. It returns Nothing if the uni cation of (t1,t2) relative to the substitution s is not possible. The empty answer list that eqn returns in this case causes the system to ignore this branch and so simulate backtracking. If the uni cation of (t1,t2) relative to the substitution s is possible, then unify returns the most general uni er u as a part of the expression Just u. In this case eqn returns a pair [u,n], where n is the number of variables used so far. This number does not change in eqn, but is necessary for the correct behaviour of the exists function so it has to be passed on as a part of the answer. Introduction of fresh variables for uni cation is implemented by exists. It takes a number of new variables as its rst argument; this is needed for ensuring fresh variable names and passing the information about the added names on to the next Predicate. exists :: Int -> ([Term] -> Predicate) -> Predicate exists k f (s,n) = f vs (s, n+k) where vs = map makevar [n..n+k-1]
Negation in Prolog is implemented through failure, and the neg function found in the appendix A implements the same idea. The implementation of the Prolog cut operator ! requires a use of continuations, and is not dealt with here. Implementing the full scope of Prolog, together with the cut operator, is one of the main topics for my further research. 21
The soundness and completeness (relative to Prolog) of the shallow embedding follows directly from the way the embedding is constructed. The described encoding is about the simplest possible mechanised formal de nition of Prolog. Any proof would really be a proof of the soundness of the other semantics.
22
Chapter 4
Algebraic Properties of the Operators In this chapter I state and discuss some properties of the combinators in the shallow embedding. Such properties are important for reasoning about Prolog predicates in this embedding, for example in program optimisation or other program transformations. The next chapter shows two examples where this technique is applied. This chapter is provisional. Some problems addressed here form the material for my further work. The main questions that could be dealt with in this chapter, but that remain outside the scope of this thesis, are reasoning about programs that return answers in the form of in nite sets, and a formulation of an algebraic semantics of (full) Prolog based on the operator laws described here.
4.1 Equality and Healthiness of Predicates The ordering of the clauses in a Prolog program, or of the literals in a clause, does not aect the declarative semantics of the program, while its procedural semantics changes. The change in the ordering of clauses or literals results in a dierent stream but the same set of computed answers for a query (as long as it has only a nite number of answers). If the query has in nitely many answers, then the computed answers are possibly dierent subsets of the same in nite set of declaratively correct answers. 23
Also, issues like whether SLD-resolution or some other computational mechanism is used, and whether depth- rst or breadth- rst search is used in the resolution, in uence the result. That is why the properties of commutativity and distributivity of the logical operators are not preserved in the implicit disjunctions and conjunctions in Prolog. Since the shallow embedding preserves nondeterminism, it is useful to dierentiate between the following two kinds of equivalence of the embedded Prolog predicates:
Stream equality implies that the answers returned form equal streams.
The number of answers and their ordering are the same. Set equality implies that the sequences of answers form the same (possibly in nite) set. In nite streams of answers and non-termination are a central point in the shallow embedding since the most interesting queries in Prolog do not terminate, and the precise formulation of the set-like equality is a problem I hope to address later. Optimally I would like to use set-equality for reasoning about embeddings of Prolog programs, but a satisfactory de nition has not been developed yet. The de nition of equality that I use in the rest of the thesis is slightly weaker than stream-equality. I allow two predicates to be equal if they return the same streams modulo variable renaming and modulo number of the auxiliary variables used. The = sign and the term \equals" (or even \stream-equals") will further on refer to this kind of equality. Predicates in the shallow embedding are functions from answers substitutions to streams of answer substitutions. Some general healthiness properties can be expressed for such functions, but a detailed study of such properties remains a subject for future work. The healthiness properties should capture the idea that a predicate applied to any substitution always returns an extension to that substitution. In other words, for each sn in the answer stream [s1,s2,s3...] to a query (p x), the answer sn is subsumed by the input substitution x, i.e. x is more general than sn (written as x sn). This holds trivially for the no predicate since it always returns an empty list of substitutions and for the yes predicate since it returns the list consisting only of the input substitution. The predicate eqn(a,b) takes an input substitution and performs uni cation of a and b relative to that substitution, so it is healthy by de nition. All the other predicates are built inductively from the three predicates above, connected with operators |||, &&& and v
24
exists,
and the healthiness of these other predicates can be established by structural induction. I sketch an informal argument below. In the argument I refer to the input to the predicates as \the input substitution", and ignore the additional information about the number of variables used so far. The induction hypotheses states that for any predicate p and any input x, x. (p x) extends x (I.H.) in other words, that p is healthy. The base cases are the predicates no, yes and eqn(a,b). As discussed above, these are healthy. There are three cases for the induction step, depending on whether the top combinator in the predicate is |||, &&& or exists. 8
1. By de nition of |||, (p ||| q)x = (p x) ++ (q x). By the induction hypothesis, p and q are healthy so (p x) and (q x) both extend x. Then their concatenation also extends x. 2. By de nition of &&&, (p &&& q)x = concat(map q(p x)). By the induction hypothesis, (p x) extends x, i.e. each of the substitutions in the list is subsumed by x. Again by induction hypothesis, q is healthy so all the resulting substitutions from q are subsumed by the corresponding input substitutions. Since subsumption is transitive each of the results after q is subsumed by the input substitution x, and concat does not change this by connecting them in a single list. 3. By de nition of exists, the expression (exists n f), where n is an integer and f is an abstraction of a predicate p taking n variables, applies f to a list n automatically generated fresh variables and returns the resulting predicate p. For example, it is typically used in the form (exists 1(\[y]-> p(y)). The exists-expression inserts one fresh variable, say z in the lambda-expression and returns the predicate p(z). The variable z is guaranteed to be fresh by implementation. By induction hypothesis, the predicate p(y) is healthy and returns only extensions of the input substitution. Because z is fresh, replacing y with it can not spoil any already computed substitution, but only extend it, so the resulting predicate is healthy too. The article [PP91] describes some related results on the \sequence of answer substitutions semantics" and \set of answer substitutions semantics" of logic programs, and discusses under which syntactical restrictions the transformation rules of Prolog preserve the rst semantics. Other related work on the semantics of logic programs is listed in chapter 6. 25
I now go on to describing some algebraic properties of the shallow embedding operators eqn, and, or and exists.
4.2 Uni cation and Substitution The operator eqn performs a uni cation of two terms relative to an exiting substitution. In other words, eqn(a,b), given an input substitution tries to nd a substitution such that extends and the results of applying to a and b are identical, a = b. If such uni cation is possible, eqn will always extends the input substitution with the most general uni er of the given terms; if not, it will return an empty list. The implementation of the uni cation used in the shallow embedding is based on J.A. Robinson's algorithm as described in [Rob65]; the code can be found in appendix A. The properties of eqn listed below are direct consequences of the well known properties of uni cation and the implementation of the shallow embedding.
;
eqn(a,b) = no
eqn(a,a) = yes
if a and b are not uni able
eqn(a,b) = eqn(b,a)
exists 1 (n[b]->(eqn(a,b) &&& eqn(b,c))) = eqn(a,c)
(4.1) (4.2) (4.3) (4.4)
The last two observations are used in some of the proofs in the next section. Note that these observations assume that the equality is up to variable renaming, and up to the number of auxiliary variables used. A more detailed study of the properties of substitutions and uni cations can be found in [Ede85].
4.3 Basic Combinators One of the strengths of the shallow embedding is that all the properties of predicate combinators ||| and &&& follow in a fairly straightforward manner from their de nition and from the standard properties of Gofer list operators. For better readability of the stated properties and the proofs I assume the following precedence order, from highest to lowest: function application (also concat and map), ++, function composition, &&&, |||, and =. 26
For easier labelling of the steps in the proofs below I repeat the de nitions of the shallow embedding operators: = [x] x = []
yes x no
= (p x) ++ (q x = concat (map
(p ||| q) x
x)
(p &&& q)
q (p x))
(4.5) (4.6) (4.7) (4.8)
The following laws for concat, map and ++, and the de nition of functional composition are given in [BW88]: concat (xs ++ ys) = (concat xs) ++ (concat ys) map f (xs ++ ys) = (map f xs) ++ (map f ys)
map f.concat = concat.map (map f)
concat.concat = concat.map concat
map (f.g) = (map f).(map g)
(f.g)x = f(g(x))
(4.9) (4.10) (4.11) (4.12) (4.13) (4.14)
The following properties of the operator ||| will be useful for proving some more complex properties: concat.(p ||| q) = concat.p ||| concat.q map r.(p ||| q)
= map
r.p ||| map r.q
(p ||| q).f = p.f ||| q.f
(4.15) (4.16) (4.17)
The rst two equalities follow directly from the combination of (4.7) with (4.9) and (4.10) respectively. The last one follows from the de nition of ||| and the de nition of functional composition (4.14): ((p ||| q).f)x
= (p ||| q)(f(x)) = (p(f(x))) ++ (q(f(x))) = (p.f)x ++ (q.f)x = (p.f ||| q.f)x
by (4.14) by (4.7) by (4.14) by (4.7)
The functions yes and no are identity elements respectively for operators 27
&&&
and |||. The function no is a zero element for &&&. =p p &&& yes = p no ||| p = p p ||| no = p no &&& p = no p &&& no = no
(4.18) (4.19) (4.20) (4.21) (4.22) (4.23)
yes &&& p
This follows from the properties of list operators in Gofer and de nitions of and the two operators. For example,
yes, no
(yes &&& p) x
= concat = concat = px
by (4.8) by (4.5) by (defn of map and concat)
(map p (yes x)) (map p [x])
On the other hand, yes is not a zero element for |||: = yes yes = yes
yes ||| p
6
p |||
6
For counterexample, assume that there is a non-empty input x where (p x) returns one answer. Then, given input x, the answer list on the left-hand side of each inequality has length two, while the right-hand side would has length one. A predicate in a &&&- or a |||-composition with itself does not return the same stream of answers. =p p=p
p &&& p p |||
6
6
Why the second equality does not hold is easy to see by counting the number of answers on both sides of the inequation. Assume that p is the (yes ++ yes) predicate, so it simply duplicates its input substitution. Then the number of answers is twice larger on the left-hand side in both &&& and ||| case. In a set-equality model both statements would turn into equalities, since all the answers on the left-hand side are repetitions of the ones on the right-hand side, because of the healthiness property of predicates. 28
The operators &&& and ||| are not commutative. =q q=q
p &&& q p |||
6
6
&&& p ||| p
Again, if set-equality was used, both ||| and &&& would have been commutative; the answers returned on both sides are the same but the ordering they are computed in is dierent. More precisely, ||| would be commutative because the answer lists on both sides contain exactly the same elements, and &&& would be commutative because the healthiness properties of predicates and commutativity of eqn ensure that the answers are equal up to variable renaming. The operators &&& and ||| are associative: =p r=p
(p &&& q) &&& r
&&& (q &&& r)
(p ||| q) |||
||| (q ||| r)
(4.24) (4.25)
The associativity of ||| follows from the associativity of list concatenation in Gofer. The associativity of &&& is proved by the following rewriting (the point of rewriting is underlined in the non-obvious cases): (p &&& q) &&& r
= concat.map r.(p &&& q) = concat.map r.concat.map q.p = concat.concat.map(map r).map q.p = concat.concat.map(map r.q).p = concat.map concat.map(map r.q).p = concat.map(concat.map r.q).p = concat.map (q &&& r).p = (p &&& (q &&& r))
by (4.8) by (4.8) by (4.11) by (4.13) by (4.12) by (4.13) by (4.8) by (4.8)
The &&& distributes over ||| from the right but not from the left: = (p q) = (r
(p ||| q) &&& r r &&& (p |||
6
&&& r) ||| (q &&& r) &&& p) ||| (r &&& q)
(4.26) (4.27)
The rst statement follows directly from the de nition of &&& and the dis29
tributivity of concat and map over |||: (p ||| q) &&& r
= concat.map r.(p ||| q) = concat.(map r.p ||| map r.q) = concat.map r.p ||| concat.map = (p &&& r) ||| (q &&& r)
r.q
by (4.8) by (4.16) by (4.15) by (4.8)
The inequality in (4.27) is due to the stream-equality model. Using the de nitions of |||, &&& and equations (4.15) we get: r &&& (p ||| q)
= concat.map (p ||| q).r = concat.((map p).r)|||((map q).r) = concat.map p.r ||| concat.map q.r = (r &&& p) ||| (r &&& q) 6
by (4.8) by (*) by (4.15) by (4.8)
The step labeled by (*) would be justi ed in the set-equality model, because the two sides of the inequation contain the same elements in dierent order. The distributivity of ||| over &&& does not hold in the stream-equality model. As the simplest counterexample let p in the inequation below have one answer for some input x, and let q and r be the no predicate. Then, given input x, the left-hand side returns one answer, while the right-hand side returns two. p ||| (q &&& r)
= (p|||q) 6
&&& (p|||r)
4.4 Variable Introduction The following properties of exists function are used in the rewriting examples in the next chapter. They follow from the de nition of the exists operator and general properties of lambda-expressions. Predicates can be moved in and out of a scope of an exists operator as long as none of their free variables become bound: x
62
p
)
exists 1 (n[x]->p) = p
30
(4.28)
Bound variables can be renamed in exists. This holds because of the more general ( [x]->p x) = ( [y]->p y). n
y
62
n
p
exists 1 (n[x]->p x)
)
= exists
1 (n[y]->p y)
(4.29)
The \one-point rule" holds because of the healthiness property of predicates: exists 1 (n[x]->eqn(x,t) &&& p(x))
=
p(t)
(4.30)
The operators ||| and &&& distribute over exists: x
62
;
q y
62
p
)
exists 1 (n[x] -> p x) &&& exists 1 (n[y] -> q y)
= exists
x
62
;
q y
62
p
2 (n[x,y] -> p x &&& q y) )
exists 1 (n[x] -> p x) ||| exists 1 (n[y] -> q y)
= exists
(4.31)
2 (n[x,y] -> p x ||| q y)
31
(4.32)
Chapter 5
Reasoning about Predicates This chapter studies two examples taken from [Spi96]. The examples show the use of the shallow embedding for proving the equivalence of two Prolog programs. This is done by rewriting the respective functions to equivalent recursive forms; the rewriting rules are the algebraic laws from the previous chapter. The corresponding proofs in Prolog use a technique called unfold-fold. The idea is to resolve the auxiliary predicates in the rst predicate (i.e. unfold it) until an instance of the body of the second predicate appears, and then replace the body with the respective head literal (i.e. fold it). The folding step is not justi ed by rst-order logic alone, because it involves a step in a reverse direction of the implication sign. The formal justi cation why this step is nevertheless correct involves comparing the least models for the predicates and the resolution trees.
5.1 Example with Adjacent Elements of the List The Prolog predicate elem(i,list,x) is satis ed if x is the i-th element of list. This predicate is de ned as: elem(0,[X|A],X). elem(J,[X|A],Y):- elem(I,A,Y), succ(J,I).
The shallow embedding of elem, accoring to the explanation in chapter 3, is: 32
elem :: (Term,Term,Term) -> Predicate elem(p,q,r) = (exists 2 (\ [x,a] -> eqn(p,zero) &&& eqn(q,(cons x a)) &&& eqn(r,x))) ||| (exists 5 (\ [i,j,x,y,a] -> eqn(p,j) &&& eqn(q,(cons x a)) &&& eqn(r,y) &&& elem(i,a,y) &&& eqn(j,(succ i))))
Two elements are adjacent in a list if they have consecutive positions in the list. We can de ne a Prolog predicate adj1(list,x,y), meaning that x is followed by y in list, using the elem predicate: adj1(A,X,Y):- elem(I,A,X), succ(J,I), elem(J,A,Y).
The shallow embedding of adj1 is: adj1 :: (Term,Term,Term) -> Predicate adj1(p,q,r) = (exists 5 (\ [i,j,x,y,a] -> eqn(p,a) &&& eqn(q,x) &&& eqn(r,y) &&& elem(i,a,x) &&& eqn(j,succ i) &&& elem(j,a,y)))
A declaratively equivalent Prolog predicate can be more simply de ned without the use of elem predicate: the elements X and Y are adjacent in a list if they are adjacent in the front of the list or if they are adjacent in the tail. The Prolog predicate adj2 implements this de nition: adj2([X|[Y|A]],X,Y). adj2([U|A],X,Y):- adj2(A,X,Y).
The shallow embedding of adj2 is: adj2 :: (Term,Term,Term) -> Predicate adj2(p,q,r) = (exists 3 (\ [x,y,a] -> eqn(p,(cons x (cons y a))) &&& eqn(q,x) &&& eqn(r,y))) ||| (exists 4 (\ [x,y,u,a] -> eqn(p,(cons u a)) &&& eqn(q,x) &&& eqn(r,y) &&& adj2(a,x,y)))
33
We want to show that the Gofer functions adj1 and adj2 are equivalent. We start with adj1, and expand elem(i,a,x) and elem(j,a,y). This gives: (exists 5 (\ [i,j,x,y,a] -> eqn(p,a) &&& eqn(q,x) &&& eqn(r,y) &&& (exists 2 (\ [x_1,a_1] -> eqn(i,zero) &&& eqn(a,(cons x_1 a_1)) &&& eqn(x,x_1))) ||| (exists 5 (\ [i_2,j_2,x_2,y_2,a_2] -> eqn(i,j_2) &&& eqn(a,(cons x_2 a_2)) &&& eqn(x,y_2) &&& elem(i_2,a_2,y_2) &&& eqn(j_2,(succ i_2)))) &&& eqn(j,succ i) &&& (exists 2 (\ [x_3,a_3] -> eqn(j,zero) &&& eqn(a,(cons x_3 a_3)) &&& eqn(y,x_3))) ||| (exists 5 (\ [i_4,j_4,x_4,y_4,a_4] -> eqn(j,j_4) &&& eqn(a,(cons x_4 a_4)) &&& eqn(y,y_4) &&& elem(i,a_4,y_4) &&& eqn(j_4,(succ i_4))))))
A series of rewriting steps follows, where one uses the the distributivity of &&& through ||| three times, in addition to the commutativity and associativity of &&& and ||| and the properties of eqn and exists to end up with four top |||-branches: (exists 5 (\ [i,j,x,y,a] -> (exists 4 (\ [x_1,a_1,x_3,a_3] -> eqn(p,a) &&& eqn(q,x) &&& eqn(r,y) &&& eqn(j,succ i) &&& eqn(i,zero) &&& eqn(a,(cons x_1 a_1)) &&& eqn(x,x_1) &&& eqn(j,zero) &&& eqn(a,(cons x_3 a_3)) &&& eqn(y,x_3))) ||| (exists 7 (\ [i_2,j_2,x_2,y_2,a_2,x_3,a_3] -> eqn(p,a) &&& eqn(q,x) &&& eqn(r,y) &&& eqn(j,succ i) &&& eqn(i,j_2) &&& eqn(a,(cons x_2 a_2)) &&& eqn(x,y_2) &&& elem(i_2,a_2,y_2) &&& eqn(j_2,(succ i_2)) &&& eqn(j,zero) &&& eqn(a,(cons x_3 a_3)) &&& eqn(y,x_3))) ||| (exists 7 (\ [x_1,a_1,i_4,j_4,x_4,y_4,a_4] -> eqn(p,a) &&& eqn(q,x) &&& eqn(r,y) &&& eqn(j,succ i) &&& eqn(i,zero) &&& eqn(a,(cons x_1 a_1)) &&& eqn(x,x_1) &&& eqn(j,j_4) &&& eqn(a,(cons x_4 a_4)) &&& eqn(y,y_4) &&& elem(i,a_4,y_4) &&& eqn(j_4,(succ i_4)))) ||| (exists 10 (\ [i_2,j_2,x_2,y_2,a_2,i_4,j_4,x_4,y_4,a_4] -> eqn(p,a) &&& eqn(q,x) &&& eqn(r,y) &&& eqn(j,succ i) &&& eqn(i,j_2) &&& eqn(a,(cons x_2 a_2)) &&& eqn(x,y_2) &&& elem(i_2,a_2,y_2) &&& eqn(j_2,(succ i_2)) &&& eqn(j,j_4) &&& eqn(a,(cons x_4 a_4)) &&& eqn(y,y_4) &&&
34
elem(i,a_4,y_4) &&& eqn(j_4,(succ i_4))))))
The rst and the second branch contain a eqn(j,succ i) and eqn(j,zero) connected with a &&&. These predicates are not uni able and their conjunction thus equals to no, which in turn is an identity element for |||, so those two branches disappear. Applying the rules for eqn the third and the fourth branch can rewrite to: (exists 3 (\ [x,y,a_4] -> eqn(q,x) &&& eqn(r,y) &&& eqn(p,(cons x a_4)) &&& elem(zero,a_4,y))) ||| (exists 5 (\ [x,y,i_2,a_2,x_2] -> eqn(q,x) &&& eqn(r,y) &&& eqn(p,(cons x_2 a_2)) &&& elem(i_2,a_2,x) &&& elem((succ i_2),a_2,y)))
The last line in the second branch is simply an instance of adj1. The expansion of elem(zero,a 4,y) in the rst branch, followed by a few steps of distribution and uni cation, results in: (exists 3 (\ [x,y,a_5] -> eqn(q,x) &&& eqn(r,y) &&& eqn(p,(cons x (cons y a_5))))) ||| (exists 4 (\ [x,y,a_2,x_2] -> eqn(q,x) &&& eqn(r,y) &&& eqn(p,(cons x_2 a_2)) &&& adj1(a_2,x,y)))
After renaming the bound variables this is equivalent to the de nition of adj2(p,q,r). We assume that two functions with the same recursive de nition are the same, and conclude the proof of the equivalence of adj1 and adj2.
5.2 Example with Reversing of a List A Prolog predicate rev1 that reverses a list can be de ned as: rev1([],[]). rev1([X|A],C):- rev(A,B), append(B,[X],C).
The order of this predicate is n2 , where n is the length of the list to be reversed. The reversing of a list can be implemented more eciently, with 35
order n, by the predicate rev2 given below. This implementation uses an accumulator list to build up the reversed list. This accumulator is local to the auxiliary predicate revapp; the predicate append is same as de ned earlier. rev2(A,B):- revapp(A,[],B). revapp(A,C,D):- rev(A,B), append(B,C,D).
The proof of the equivalence of rev1 and rev2 is not as straight forward as in the previous example, since rewriting alone is not sucient to complete it. A lemma that states a consequence of the associativity of append is needed: Appending an element to the end of the second list is the same as appending an element to the result. The proof of this lemma requires structural induction and is omitted here. In the shallow embedding this property is expressed as: append(a,b,c) &&& append(c,(cons x nil),d) = (exists 1 (\ [e]-> append(b,(cons x nil),e) &&& append(a,e,d)))
An alternative de nition of the revapp predicate is also required. It can be proved equivalent to the de nition of revapp applying the same technique as in the main examples in this chapter. revapp2([],B,B). revapp2([X|A],B,C):- revapp2(A,[X|B],C).
The proof of the equivalence of the two reverse predicates in the shallow embedding can, for example, start from the shallow embedding of the rev1 predicate: rev1(p,q) = (eqn(p,nil) &&& eqn(q,nil)) ||| (exists 4 (\ [x,a,b,c]-> eqn(p,(cons x a)) &&& eqn(q,c) &&& rev1(a,b) &&& append(b,(cons x nil),c)))
Unfolding rev1(p,q) and using the associativity and distributivity laws, then applying the laws about uni cation and variable elimination, gives: 36
(eqn(p,nil) &&& eqn(q,nil)) ||| (exists 4 (\[x,a,b,c] -> eqn(p,(cons x nil)) &&& eqn(q,(cons x nil)) ||| (exists 4 (\[x_1,a_1,b_1,c_1] -> eqn(p,(cons x (cons x_1 a_1))) &&& eqn(q,c) &&& rev1(a_1,b_1) &&& append(c_1,(cons x nil),c) &&& append(b_1,(cons x_1 nil),c_1)))
The last two lines are an instance of the left-hand side of append lemma, so those lines can rewrite to: (exists 1 (\[e] -> append((cons x_1 nil),(cons x nil),e) &&& append(b_1,e,c))
Unfolding the new append instances and using the usual laws to simplify the result gives: rev1(p,q) = (eqn(p,nil) &&& eqn(q,nil)) ||| (exists 1 (\[x] -> eqn(p,(cons x nil)) &&& eqn(q,(cons x nil)) ||| (exists 4 (\[x_1,a_1,b_1,c] -> eqn(p,(cons x (cons x_1 a_1))) &&& eqn(q,c) &&& rev1(a_1,b_1) &&& append(b_1,(cons x_1 (cons x nil)),c)))
I call this equation (*). Now we turn to the shallow embedding of rev2: rev2(p,q) = (exists 2 (\[a,b] -> (eqn(p,a) &&& eqn(q,b) &&& revapp(a,nil,b)))
Using the equivalence of revapp and revapp2, this expression can be rewritten to an instance that uses revapp2 instead. Unfolding revapp2 twice and using the usual rewrite rules to simplify results in: (eqn(p,nil) &&& eqn(q,nil)) |||
37
(exists 4 (\[x_1,a_1,b_1,c_1] -> (eqn(p,(cons x_1 nil)) &&& eqn(q,(cons x_1 nil))) ||| (exists 4 (\[x_3,a_3,b_3,c_3] -> (eqn(p,(cons x_1 (cons x_3 a_3))) &&& eqn(q,c_3) &&& revapp2(a_3,(cons x_3 (cons x_1 nil)),c_3))))
To make this equivalent to the right-hand side of (*), the occurrence of
revapp2 must dissappear, so using the equivalent revapp de nition to unfold
once and then simplifying nally gives:
rev2(p,q) = (eqn(p,nil) &&& eqn(q,nil)) ||| (exists 1 (\[x_1] -> (eqn(p,(cons x_1 nil)) &&& eqn(q,(cons x_1 nil))) ||| (exists 4 (\[x_3,a_4,b_4,d_4] -> (eqn(p,(cons x_1 (cons x_3 a_4))) &&& eqn(q,d_4) &&& rev2(a_4,b_4) &&& append(b_4,(cons x_3 (cons x_1 nil)),d_4))))
This two recursive de nitions is equal to (*) up to variable renaming. With the assumption that two functions with equivalent de nitions are the equivalent we conclude the proof of equivalence of predicates rev1 and rev2. Comparing the proofs presented here with the corresponding proofs for simple functional programs, we see that the proofs presented in this thesis are more cumbersome, but the theorem is more powerful. It applies to relational uses of the predicate as well as purely the functional result. One nal remark to this chapter: all the rewriting was done by hand, but it could hopefully be automatized using a mechanized rewriting tool similar to the one that G. Sittampalam is developing. The full text of the proofs is in the appendix C. They could be somewhat shortened by using a simpler representation for the predicates. For example, adj1 could alternatively be written as: adj1 :: (Term,Term,Term) -> Predicate adj1(p,q,r) = (exists 1 (\ [i] -> elem(i,p,q) &&& eqn(j,succ i) &&& elem(succ(i),p,r)))
This representation would help to eliminate unnecessary variables, and make the rewriting shorter and simpler. I have ultimately chosen not to use this 38
shortening of predicates because the notation I use follows more closely the explanation of the shallow embedding given in chapter 3.
39
Chapter 6
Conclusions, and DPhil Proposal An important part of my further work is to compare and contrast the shallow embedding with the other work done in the area of integration of functional and logic programming paradimgs. In this chapter I list the related articles and describe the possible directions for further research on the shallow embedding.
6.1 Related Work The most cited survey article on the uni cation of functional and logic languages is [Han94]. Other survey articles on the relation between functional and logic languages and their integration are [BL86, DFP86, WP00, DGP92, Llo94, Llo98] and [Red87]. Some recently developed languages that combine the two paradigms are Babel [MNRA92], Kernel-Leaf [GLMP91], Escher [Llo95] and Curry [HKMN95]. Languages that are specially designed for higher-order logic programming are Lambda-Prolog [Mil91, NM88, GH95], Mercury [SHC95] and HiLog [CKW93]. Loglisp [RS82, Rob88, CT82] by A.J. Robinson is a simmilar approach to the shallow embedding in that it uses an existing functional language (Lisp) to model the behaviour of logic programs, where a set of logic programming primitives is implemented as functions. The shallow embedding's predicate combinators and, or and exists are in 40
a way similar to L.C. Paulson's tacticals (if the predicates in the shallow embedding are interpreted as tactics) [Pau85], but the tactics are generally used to pass on the unresolved parts of proofs while the combinators in the shallow embedding are used to pass on partial answers to queries. There has been a considerable amount of research on the lazy computations in logic programming languages. Some examples are described in [Lut89], [Nar86] and [Ant91]. Regarding the semantics of logic programs, a stepwise development of operational and denotational semantics for Prolog is described in [JM84], and the use of algebraic semantics for proving Prolog termination and transformation is described in [Ros00] (using CCS to model Prolog). Other interesting articles include: [vEK76], [GM87] and [PP91]. Egon Borger uses evolving algebras for a semantical analysis of the logic programs, with case studies on (full) Prolog, BABEL and Godel [Bor94, BLR94, BR94]. For general background reading I have used the following books:
logic programming and Prolog: [Spi96, Llo93, Apt97, SS86, AdBR93, Hog90] (and articles [Rob65, Sha89]) functional programming and Gofer: [BW88, FWH92] unifying of the theories of programming: [HJ98] introduction to operational and algebraic semantics: [Hoa85, Ros98]
6.2 Further Work Because of its operational simplicity and transparency, the shallow embedding opens many possibilities for further research. Some of the most interesting ones are:
reasoning about programs with in nite streams of answers implementing the shallow embedding for full Prolog implementing dierent-search strategies in the shallow embedding higher order programming in the shallow embedding
Below I describe each of these topics is some detail. 41
6.2.1 Set-Equality in the Shallow Embedding Two logic programs can have the same declarative semantics even if they do not return the answers in the same order, or even if they do not return the same set of answers. The latter case can, for example, occur when one of the queries does not terminate. Therefore, a concept of set-like equality of the shallow embeddings of Prolog programs would be useful. With this kind of equality, some useful properties of operators can be established in addition to those that hold with streamequality. For example, ||| and &&& become commutative and distributive in the set-equality model. In the shallow embedding the results are modeled as streams of answers. The set-equality between two predicates can be de ned in terms of subsumption ( ), as long as both streams are nite. Two predicates are set-equal i the streams of their answer substitutions subsume each other, or more precisely: let be the result of the shallow interpretation of p1 and be the result of the shallow interpretation of p2. Then p1 and p2 are set-equal (denoted by =) i: p1 = p2 v
,
v
^
v
where the stream subsumption is de ned as t : s :s t, and for any two substitutions t and s, s subsumes t i s is more general, i.e. i s can be composed with some substitution p to get t: s t p:t = s . p v
v
, 8
2
9
2
v
, 9
This de nition of set-equality assumes the set-membership test for the substitutions in the two sets and . But membership is only semi-decidable for in nite sets, so this de nition is not applicable to in nite computations. A similar problem occurs whenever one deals with programs or functions that return in nite streams. How does one check the equality of two such programs when all one has are nite computed subsets of in nite sets of correct answers? Some work in this direction has been done in [BdM97] and will be the basis for further work.
6.2.2 The Implementation of Full Prolog The shallow embedding presented so far does not implement the operator cut nor the other non-logical features of Prolog. Whether the non-logical 42
features, like assert, retract, var and nonvar, should have been introduced in Prolog in the rst place is a topic of an ongoing debate. It would be an interesting, though for our purposes tangential, topic for further research to see how well these features can be incorporated in the shallow embedding. The operator cut is central in \real-world" programming with Prolog. It is used as a control device for the search process: executing a cut prevents some part of the search-tree from being examined. In general, cuts are used in order to prune out computations which either produce no answers or else produce unwanted answers. An simple treatment of cut is possible in the existing model. The function cut is implemented as a function on predicates, and its behaviour can be de ned as truncation of the answer list after the rst element: cut :: Predicate -> Predicate cut (p x) = take 1 (p x)
This function does not have the same behaviour as Prolog's cut when backtracking is involved. In the example below I use ! which is the denotation for cut in most common Prolog dialects. Given a Prolog program: p :- q, !, r. p :- s. q :- a. q :- b. a :- .
The execution of the ! causes all the shaded parts in the gure below to be pruned from the search-tree. p q,!,r a,!,r
s
b
!,r r ...
43
When translated to the shallow embedding the clause p would correspond to (cut(q) &&& r) ||| s, where the calculation of s is not aected by the execution of cut. A model that preserves the proper behaviour of cut requires the use of continuations and is inspired by [Wan80]. The questions of how the shallow embedding operators will have to change to accommodate this behaviour is a subject of further research.
6.2.3 Exploring Dierent Computation Strategies Another direction of research is the incorporation of dierent selection and computation strategies in the shallow embedding. The de nitions of ||| and &&& in the shallow embedding are: (p ||| q) x = (p x) ++ (q x) (p &&& q) x = concat (map q (p x))
The implementation of |||, together with the laziness of Gofer causes the search of answers to behave like the depth- rst search of the search-tree in Prolog. Obviously, all the answers corresponding to the (p x) part of the search tree are returned before the the other part is explored. The other part of the search-tree might never be reached. A more fair computation strategy would share the computation load more evenly between the two parts. Also, the implementation of &&& results in a left-to-right selection of the literals of a clause, where all the answers for the rst literal are computed before computing the answers for the second literal etc. A more fair selection strategy would compute one answer for each of the literals before considering the rst literal for the second time. One possible solution is to use the laziness of Gofer to interleave the streams of answers, taking one answer from each stream in turn. A function twiddle (inspired by [McP98]) that interleaves two lists can be de ned in Gofer as: twiddle :: [a] -> [a] -> [a] twiddle [] ys = ys twiddle (x:xs) ys = x:(twiddle ys xs)
The operators ||| and &&& can be de ned in terms of twiddle (note that concat = foldr (++) []): 44
(p ||| q) x = twiddle (p x) (q x) (p &&& q) x = foldr (twiddle) [] (map q (p x))
The rst line corresponds to a fair computation strategy and the second one corresponds to a fair selection strategy. The abstraction level of the shallow embedding allows one to choose between the dierent fairness settings at the query time, whereas in Prolog interpreters such decisions have to be hard-coded. The idea is to parametrise the de nitions of ||| and &&& to take twiddle or ++ as an input argument. For example, to express that a depth- rst search of the search-tree (standard use of |||) and a fair selection rule (fair use of &&&) should be used in a given query, one would write: solve (++) (twiddle) query
The function prolog, used for solving queries in the implementation of shallow embedding given in appendix A, could thus be rede ned as solve (++) (++). This notation underlines the depth- rst, left-to-right nature of Prolog. The decision whether the fair selection or computations should be used is a trade-o between eciency of the query and its termination. The analysis of the termination of Prolog queries, depending on the mode of the parameters in the query, can be automated, as for example in [McP98]. This approach attempts to solve queries with optimal eciency and optimal termination. An interesting side issue here is how much the typing system of Gofer facilitates the termination-analysis for the predicates in the shallow embedding. Note that the fair use of ||| does not give breadth- rst search of the searchtree; it deals with in nite success but not with in nite failure. In other words, if gives fair behaviour in in nite search-trees where all the derivations are nite and eventually resolve into success or failure, but if a tree contains an in nite branch the query will \get stuck". To nd the answers in trees with in nite branches, one has to ensure that after a certain number of resolution steps in the rst branch, say one, the second branch should be used for computation of equally many steps. This is not possible to implement in the simple model of shallow embedding that is presented in this thesis. Even in the interleaved implementations, the laziness still requires the rst element of the answer list, which is reached after an arbitrary (possibly in nite) number of steps, before the other branch or literals are considered. Consider the following Prolog program: 45
r:- p. r:- q. p:- p. q.
The query r? does not terminate in the model above, even with the twiddle parameter for |||, since the search-tree has an in nite rst branch and the second branch is never reached. The same query would terminate in a breadth- rst search, as the nodes of search-tree are traversed in the order indicated in the gure below. r1 p2
q3
p4
5
p6 ...
To implement breadth- rst search in the shallow embedding, the Predicate data-type needs changing. It is no longer adequate to return a single, at stream of answers; as discussed above, this model is not re ned enough to take into account the number of computation steps needed to produce a single answer. The key idea in the new model is to make Predicate return a stream of stream of answers, where each stream represents the answers reached at the same depth, or level, of the search tree. This allows the fair distribution of the computation steps between the branches of the tree: rst all the branches are explored in one step and any answers returned, then the computation is taken one step further in each branch etc. The new type of Predicate is thus: Predicate :: Answer -> [[Answer]]
The Gofer notation does not dierentiate between nite lists and streams, but the type of a Predicate is really a stream of nite lists of answers, since there is only a nite number of nodes at each level of the search tree, and therefore only a nite number of possible answers on each level. 46
Intuitively, each of the lists of the answers in the main list correspond to the answers with the same computational \cost". The cost of an answer increases with every resolution step in its computation. This can be captured by adding a new function call step in the de nition of the predicates in the shallow embedding. For example, append should in this breadth- rst model be coded as: append(p,q,r) = step (exists 1 (\ [x] -> eqn(p,nil) &&& eqn(q,x) &&& eqn(r,x))) ||| step (exists 4 (\ [x,a,b,c] -> eqn(p,(cons x a)) &&& eqn(q,b) &&& eqn(r,(cons x c)) &&& append(a,b,c)))
where step is de ned as: step :: Predicate -> Predicate step p x = []:(p x)
The implementations of the Predicate combinators ||| and &&& need altering since they no longer operate on lists but on streams of lists. They must preserve the cost information that is embedded in the input lists. Since the cost corresponds to the level of the answer in the search tree, only resolution steps (i.e. the calls for step) are charged for, while the applications of |||, &&& and eqn are cost-free. The ||| simply zips the two main streams into a single one, by concatenating all the sublists of answers with the same cost. The zipping must not stop when it reaches the end of the shortest of the two lists (two streams may well be of dierent lengths). The function fairzipwith accommodates such behaviour. (p ||| q) x = fairzipwith (concat) (p x) (q x)
The implementation of &&& is harder, since the cost of each of the answers to (p &&& q) is a sum of the costs of the computation of p and the computation of q. The idea is to rst compute all the answers, and then to atten the resulting [[[[Answer]]]] to a [[Answer]] according to the cost. The
attening is done by the shuffle function which is explained below. 47
p &&& q = shuffle . map (map q) . p
I write S for streams and L for nite lists in the explanation below. The type of the result of map (map q).p is a stream of lists of streams of lists, i.e. SLSL. It can be visualised as a matrix of matrices, where each of the inner matrices corresponds to a single answer of p. Each such answer is used as an input to q and consequently gives rise to a new stream of lists of answers, which are represented by the elements of the inner matrix. All the answers of p with cost 0 will be in the rst row of the outer matrix, and all the answers of map (map q).p with cost 0 will be on the rst row of each of the inner matrices in the rst row of the outer matrix. All the rows of the outer matrix and the sub-matrices are nite, while the columns of both can be in nite. The answers with cost 2 are marked in the drawing below:
The function shuffle collects all the answers marked in the drawing in a single list element of the resulting stream of answers. It is given a stream of lists of streams of lists of answers (SLSL), and it returns a single stream of lists (SL). Two auxiliary functions are required to do this: diag and transpose. diag :: -> [[a]] -> [[a]] diag (xs:xss) = zipwith (:) xs ([]:diag xss) transpose :: [[a]] -> [[a]] transpose xss = map hd xss : transpose (map tl xss)
A stream of streams is converted to a stream of lists by diag, i.e. it is of type SS->SL. A list of streams can be converted to a stream of lists by transpose i.e. it is of type LS->SL. The gure below illustrates the eect of these two functions. 48
... ...
list1
...
...
...
diag
...
...
...
list4
...
list3
...
...
list2
transpose
Given diag and transpose as above, shuffle can be implemented as follows. The input to shuffle is of type SLSL. The application of map transpose swaps the middle SL to a LS, and gives a SSLL. Then the application of diag converts the outermost SS to SL and returns SLLL. This can now be used as input to map(concat.concat) which attens the three innermost levels of lists into a single list, and returns SL. shuffle =
map (concat . concat) . diag . map transpose
In this model the &&& operator is not associative (it is associative modulo permutation for nite streams of answers). Some other algebraic properties of the operators might be lost too. An issue for further research is how this aects reasoning about program transformation in the shallow embedding. To implement both depth- rst search and breadth- rst search in the shallow embedding, the model has to be further re ned. It is not sucient to implement predicates as functions returning streams of answer lists; they have to operate on forests of trees. The operators ||| and &&& are rede ned to be operations on trees, where the rst one connects two subtrees in a single tree and the second \grafts" trees with small subtrees at the leaves into normal trees. This is a topic for further research. It is interesting how concise the de nitions of ||| and &&& remain in all three models. To recapitulate the three de nitions of, for example, &&&, in the depth- rst model, breadth- rst model and the tree model which accommodates both search strategies, respectively: p &&& q = concat . map q . p p &&& q = shuffle . map (map q) . p p &&& q = graft . treemap q . p
49
It appears that one of the main strengths of the shallow embedding is how simply it presents the dierent models of Predicate with dierent power to describe computation in logic programs.
6.2.4 Higher Order Programming Implementing higher-order predicates in the shallow embedding is another possible direction for further work. The current implementation de nes the data-type Term as: data Term = Func Func [Term] | Var Var type Func = String data Var = Name String | Gen Int
That de nition could be replaced by: data Term = Var Var | Const String | Ap Term Term data Var = Name String data Const = Name String
With this de nition the term (append
x y z)
becomes:
Ap (Ap (Ap (Const "append") (Var "X"))(Var "Y"))(Var "Z")
This change of term structure requires some recoding of the parser and of the uni cation algorithm. No further extensions are needed in the shallow embedding for coding of higher-order Prolog predicates. For example, the higher-order insertion sort logic program can be implemented as: isort X Y :- foldr insert nil X Y. foldr P C nil C. foldr P C (A:X) B :- P A D B, foldr P C X D. insert A nil (A:nil). insert A (B:Y) (A:B:Y) :- leq A B. insert A (B:Y) (B:Z) :- gt A B, insert A Y Z.
This can be translated directly in the shallow embedding, and again the embedding has an advantage of being very simple. Also, being that all the higher-order predicates are bound to terms in the embedding, the typesystem of Gofer can be used to facilitate the type-checking. 50
6.3 Conclusions There has been a long and ongoing eort to combine the two most important paradigms of declarative programming, functional and logic programming. The two primary goals of this research are to make tools that exploit the most powerful concepts from both paradigms and to gain a better understanding of declarative computing. It is a fruitfull combination; functional programming contributes higher-order functions and more ecient operational behaviour (because of determinism) whereas logic programming contributes function inversion, partial data structures and logical variables. It is hoped that this integration can reduce the duplication of research and the fragmentation in the eld of declarative programming. The integration has been approached through the implementation of languages that combine concepts from logic and functional programming. This is a worthwhile eort, since it provides programmers with ecient hybrid tools for declarative programming. Unfortunately, most of those implementations lack the clarity that the shallow embedding possesses. For example, the Escher system employs 114 rewrite rules, and in the description of the language [Llo94] J.W. Lloyd writes that \there are far too many rewrites to give a comprehensive account of them". In contrast to this, the shallow embedding only has a few rewrite rules involving the main operators, while most of the work is left to the lazy list operators of Gofer. The work presented in this thesis is not aimed towards an implementation of a new programming language, although a language implementation based on the shallow embedding is conceivable. Rather, this work is directed towards producing and using a theoretical tool (with a simple implementation) for the analysis of dierent aspects of logic programs. The three main parts of the work presented in this thesis are:
the core implementation of the operators of the shallow embedding, the operational analysis of the combinators of the embedding, and the application of the embedding to program transformation
Each of these tasks turns out to be quite tractable in the shallow embedding, in the sense that they could be carried out in a reasonably straightforward and self-explanatory manner. Indeed, this simplicity is the key idea and the main strength of the shallow embedding. The procedural semantics of 51
the embedded program is both transparent and very close to the declarative semantics of the original logic program. The embedding is abstract enough to be exible { it permits experimenting with dierent search strategies and higher-order logic programs. It facilitates reasoning about logic programs and can be used for analysis of programs, program transformation and parallelisation of logic programs. All these are topics for further research; the scope of this work is merely to present this tool and to sketch the plethora of questions it opens.
52
Appendix A
Code for the Operators A.1 General Types > data Term = Func Func [Term] | Var Var > type Func = String > data Var = Name String | Gen Int > type Subst = [(Var, Term)] > type Predicate = Answer -> [Answer] > type Answer = (Subst, Int)
A.2 Higher Order Predicates > no :: Predicate > no x = [] > yes :: Predicate > yes x = [x] > infixl 6 ||| > (|||) :: Predicate -> Predicate -> Predicate > (p ||| q) x = (p x) ++ (q x) > infixl 7 &&& > (&&&) :: Predicate -> Predicate -> Predicate > (p &&& q) x = concat (map q (p x)) > eqn :: (Term,Term) -> Predicate
53
> eqn (t1,t2) (s,n) = > case (unify s (t1,t2)) of > Just u -> [(u,n)] > Nothing -> [] > exists :: Int -> ([Term] -> Predicate) -> Predicate > exists k f (s,n) = f vs (s, n+k) > where vs = map makevar [n..n+k-1] > neg :: Predicate -> Predicate > neg p (s,n) = yes (s,n), if res == [] > = no (s,n), otherwise > where res = p (s,n)
A.3 Integer and List Modelling > zero :: Term > zero = atom "0" > succ :: Term -> Term > succ t = Func "succ" [t] > cons :: Term -> Term -> Term > cons x a = Func "cons" [x,a] > nil :: Term > nil = atom "nil" > atom :: String -> Term > atom a = Func a [] -- plus(X,0,X). -- plus(X,S(Y),S(Z)) :- plus(X,Y,Z). > plus :: (Term,Term,Term) -> Predicate > plus (p,q,r) = > (exists 1 (\ [x] -> eqn(p,x) &&& eqn(q,(Func "0" [])) &&& eqn(r,x))) > ||| > (exists 2 (\ [x,y,z] -> eqn(p,x) &&& eqn(q,(Func "succ" [y])) &&& > eqn(r,(Func "succ" [z])) &&& plus(x,y,z))) -- minus(X,0,X). -- minus(0,X,0). -- minus(S(X),S(Y),Z) :- minus(X,Y,Z). > minus :: (Term,Term,Term) -> Predicate > minus (p,q,r) =
54
> > > > > >
(exists 1 (\ [x] -> eqn(p,x) &&& eqn(q,zero) &&& eqn(r,x))) ||| (exists 1 (\ [x] -> eqn(p,zero) &&& eqn(q,x) &&& eqn(r,zero))) ||| (exists 3 (\ [x,y,z] -> eqn(p,(Func "succ" [x])) &&& eqn(q,(Func "succ" [y])) &&& eqn(r,z) &&& minus(x,y,z)))
A.4 Auxiliary Functions > makevar :: Int -> Term > makevar i = Var (Name ("x"++(show i))) > prolog :: Predicate -> [String] -> String > prolog p vars = print vars result > where result = p startAnswer > startAnswer::Answer > startAnswer = ([],0) > print :: [String] -> [Answer] -> String > print vars ((s,n):others) = "no", if s==[] > = "yes", if (vars==[] && not (s==[])) > = instsubst s (filter (onlyinput vars) s) ++ > "\n" ++ (print vars others), otherwise > print vars [] = "" > onlyinput :: [String] -> (Var,Term) -> Bool > onlyinput vars (v,t) = True, if member vars (varname v) > = False, otherwise > all :: [String] -> (Var,Term) -> Bool > all vars (v,t) = True > member :: [String] -> String -> Bool > member [] x = False > member (y:ys) x = True, if x==y > = member ys x, otherwise > varname :: Var -> String > varname (Name v) = v > instsubst :: Subst -> [(Var,Term)] -> String > instsubst s [] = "" > instsubst s ((v,t):others) = > varname v ++ " = " ++ show (inst s t) ++ "\n" > ++ instsubst s others
55
A.5 Substitution and Uni cation > data Maybe a = Just a | Nothing > tryfold :: (a -> b -> Maybe a) -> a -> [b] -> Maybe a > tryfold f x [] = Just x > tryfold f x (y:ys) = > case f x y of > Just z -> tryfold f z ys > Nothing -> Nothing > tryassoc :: [(a, b)] -> a -> Maybe b > tryassoc [] x = Nothing > tryassoc ((u,v):ps) x = > if u == x then Just v else tryassoc ps x -- subst s t is one step of applying a substitution > subst s (Func f xs) = Func f xs > subst s (Var v) = > case tryassoc s v of > Just t -> subst s t > Nothing -> Var v -- inst s t applies substitution s to term t: it -- iterates to a fixpoint > inst s t = > case subst s t of > Func f xs -> Func f (map (inst s) xs) > Var v -> Var v -- unify does unification relative to an exiting substitution > > > > >
unify :: Subst -> (Term, Term) unify s (Var v, t2) = univar s unify s (t1, Var v) = univar s unify s (Func f a, Func g b) = if f == g then tryfold unify
-> Maybe Subst v t2 v t1 s (zip (a,b)) else Nothing
> univar s v t = > case tryassoc s v of > Just u -> unify s (u, t) > Nothing -> if t' == Var v then Just s else Just ((v,t'):s) > where t' = subst s t
56
Appendix B
Some Examples in the Shallow Embedding This chapter shows the computations referred to in chapter 3 and a mediumsized example of Prolog embedded into Gofer using the shallow embedding.
B.1 Computations with append The Prolog queries and answers are written behind a \--" for easier readability. In Gofer the function prolog is used to run the query, taking the query as its rst argument and the variables in the query as its second argument (with a better parser implementation the last argument could be omitted). The implementation of list-predicates in Gofer is brie y explained chapter 3, but for this example it suces to know that (atom "2") is a translation of a constant Prolog term 2 and (cons (atom "2") nil) is a translation of a Prolog list [2]. Var is a translation of a Prolog variable term. The results the queries in Prolog and Gofer are equal for all the three possible types of outcome of a Prolog query: successful (here divided into successful ground queries and queries with variables), failing and in nite:
successful ground queries -- append([1,2],[3,4],[1,2,3,4])? -- yes ? prolog(append((cons (atom "1") (cons (atom "2") nil)),
57
(cons (atom "3") (cons (atom "4") nil)), (cons (atom "1") (cons (atom "2") (cons (atom "3") (cons (atom "4") nil)))))) [] yes
successful queries with variables -- append([1,2],[3,4],X)? -- X = [1,2,3,4] ? prolog(append((cons (atom "1") (cons (atom "2") nil)), (cons (atom "3") (cons (atom "4") nil)), Var(Name "x"))) ["x"] x = (cons (atom "1") (cons (atom "2") (cons (atom "3") (cons (atom "4") nil)))) -- append([1,2],X,[1,2,3,4])? -- X = [3,4] ? prolog(append((cons (atom "1") (cons (atom "2") nil)), Var(Name "x"), (cons (atom "1") (cons (atom "2") (cons(atom "3") (cons (atom "4") nil ))))))["x"] x = (cons (atom "3") (cons (atom "4") nil))
failing queries -- append([1,2],X,[3])? -- no ? prolog(append((cons (atom "1") (cons (atom "2") nil)), Var(Name "x"), (cons (atom "3") nil)))["x"] no
queries with in nite answers ------
append(X,[3,4],Y)? X = [], Y = [3,4]; X = [_0], Y = [_0,3,4]; X = [_0,_4], Y = [_0,_4,3,4]; ...
? prolog(append(Var(Name "x"), (cons (atom "3") (cons (atom "4") nil)), Var(Name "y")))["x","y"]
58
y = (cons (atom "3") (cons (atom "4") nil)) x = nil y = (cons x0 (cons (atom "3") (cons (atom "4") nil))) x = (cons x0 nil) y = (cons x0 (cons x4 (cons (atom "3") (cons (atom "4") nil)))) x = (cons x0 (cons x4 nil))
B.2 A Family Database Example The example shows a simple family database that implements the predicates: mother, father, grandfather, child, sibling, cousin, and ancestor. The ancestor predicate and the earlier de ned predicate append exemplify how the shallow embedding handles recursive Prolog predicates. In the program code below the Prolog de nitions are pre xed with a -- and the Gofer de nitions are pre xed with a >. --------
mother(ilona,erzsebet). mother(ilona,marija). mother(manci,sandor). mother(erzsebet,silvija). mother(erzsebet,gabriela). mother(marija,natasa). mother(marija,ivana).
> mother :: (Term, Term) -> Predicate > mother (p,q) = > eqn(p,atom "ilona") &&& eqn(q, atom "erzsebet") > ||| > eqn(p,atom "ilona") &&& eqn(q, atom "marija") > ||| > eqn(p,atom "manci") &&& eqn(q, atom "sandor") > ||| > eqn(p,atom "erzsebet") &&& eqn(q, atom "silvija") > ||| > eqn(p,atom "erzsebet") &&& eqn(q, atom "gabriela") > ||| > eqn(p,atom "marija") &&& eqn(q, atom "natasa") > ||| > eqn(p,atom "marija") &&& eqn(q, atom "ivana") -- father(peter,sandor). -- father(istvan,erzsebet).
59
-- father(sandor,gabriela). -- father(sandor,silvija). > father :: (Term, Term) -> Predicate > father (p,q) = > eqn(p,atom "peter") &&& eqn(q, atom "sandor") > ||| > eqn(p,atom "istvan") &&& eqn(q, atom "erzsebet") > ||| > eqn(p,atom "sandor") &&& eqn(q, atom "gabriela") > ||| > eqn(p,atom "sandor") &&& eqn(q, atom "silvija") -- grandfather(X,Y) :- father(X,Z), father(Z,Y). -- grandfather(X,Y) :- father(X,Z), mother(Z,Y). > grandfather :: (Term, Term) -> Predicate > grandfather(p,q) = > (exists 3 (\ [x,y,z] -> eqn(p,x) &&& > eqn(q,y) &&& father(x,z) &&& father(z,y))) > ||| > (exists 3 (\ [x,y,z] -> eqn(p,x) &&& > eqn(q,y) &&& father(x,z) &&& mother(z,y))) -- child(X,Y) :- mother(Y,X). -- child(X,Y) :- father(Y,X). > child :: (Term, Term) -> Predicate > child(p,q) = > (exists 2 (\ [x,y] -> eqn(p,x) &&& > eqn(q,y) &&& mother(y,x))) > ||| > (exists 2 (\ [x,y] -> eqn(p,x) &&& > eqn(q,y) &&& father(y,x))) -- sibling(X,Y) :- child(X,Z), child(Y,Z), not(X=Y). > sibling(p,q) = > (exists 3 (\ [x,y,z] -> eqn(p,x) &&& > eqn(q,y) &&& child(x,z) &&& child(y,z) &&& neg (eqn(x,y)))) -- cousin(X,Y) :- child(X,Z), child(Y,W), sibling(Z,W). > cousin(p,q) = > (exists 4 (\ [x,y,z,w] -> eqn(p,x) &&& > eqn(q,y) &&& child(x,z) &&& child(y,w) &&& sibling(z,w))) -- ancestor(X,Y) :- child(Y,X). -- ancestor(X,Y) :- child(Y,Z), ancestor(X,Z).
60
> ancestor :: (Term, Term) -> Predicate > ancestor(p,q) = > (exists 2 (\ [x,y] -> eqn(p,x) &&& > eqn(q,y) &&& child(y,x))) > ||| > (exists 3 (\ [x,y,z] -> eqn(p,x) &&& > eqn(q,y) &&& child(y,z) &&& ancestor(x,z)))
B.2.1 Test run This section shows some results from the shallow interpreter. The queries below correspond to the Prolog queries: mother(erzsebet,X)?, ancestor(X,silvija)?, and cousin(gabriela,X)? A query is executed in the shallow embedding by passing a Predicate as the rst argument to the function prolog. The general format of an input question to the shallow interpreter is: prolog (Predicate) [variables]. The list of variables in the end is needed so that the system can output only those parts of the resulting substitutions which appear as variables in the input. This corresponds to the Prolog notion of the answer substitution. Passing the list of variables can be avoided with a more advanced parsing mechanism, but that was not one of the priorities in this project. The functions in the shallow embedding return the same values as their Prolog counterparts: ? prolog (mother(atom "erzsebet", Var (Name "x"))) ["x"] x = Func "silvija" [] x = Func "gabriela" [] ? x x x x x x
prolog = Func = Func = Func = Func = Func = Func
(ancestor(Var (Name "x"),atom "silvija")) ["x"] "erzsebet" [] "sandor" [] "ilona" [] "istvan" [] "manci" [] "peter" []
? prolog (cousin(Func "gabriela" [], Var (Name "x"))) ["x"] x = Func "natasa" [] x = Func "ivana" []
61
Appendix C
Rewriting for Examples from Chapter 5 C.1 The Adjacent Elements Example adj1(p,q,r) = (exists 5 (\ [i,j,x,y,a] -> eqn(p,a) &&& eqn(q,x) &&& eqn(r,y) &&& elem(i,a,x) &&& eqn(j,succ i) &&& elem(j,a,y))) = (exists 5 (\ [i,j,x,y,a] -> eqn(p,a) &&& eqn(q,x) &&& eqn(r,y) &&& (exists 2 (\ [x_1,a_1] -> eqn(i,zero) &&& eqn(a,(cons x_1 a_1)) &&& eqn(x,x_1)) ||| (exists 5 (\ [i_2,j_2,x_2,y_2,a_2] -> eqn(i,j_2) &&& eqn(a,(cons x_2 a_2)) &&& eqn(x,y_2) &&& elem(i_2,a_2,y_2) &&& eqn(j_2,(succ i_2))))) &&& eqn(j,succ i) &&& (exists 2 (\ [x_3,a_3] -> eqn(j,zero) &&& eqn(a,(cons x_3 a_3)) &&& eqn(y,x_3)) ||| (exists 5 (\ [i_4,j_4,x_4,y_4,a_4] -> eqn(j,j_4) &&& eqn(a,(cons x_4 a_4)) &&& eqn(y,y_4) &&& elem(i,a_4,y_4) &&& eqn(j_4,(succ i_4))))))) = (exists 5 (\ [i,j,x,y,a] -> (exists 2 (\ [x_1,a_1] -> eqn(p,a) &&& eqn(q,x) &&& eqn(r,y) &&& eqn(j,succ i) &&& eqn(i,zero) &&& eqn(a,(cons x_1 a_1)) &&& eqn(x,x_1)) |||
62
(exists 5 (\ [i_2,j_2,x_2,y_2,a_2] -> eqn(p,a) &&& eqn(q,x) &&& eqn(r,y) &&& eqn(j,succ i) &&& eqn(i,j_2) &&& eqn(a,(cons x_2 a_2)) &&& eqn(x,y_2) &&& elem(i_2,a_2,y_2) &&& eqn(j_2,(succ i_2))))) &&& (exists 2 (\ [x_3,a_3] -> eqn(j,zero) &&& eqn(a,(cons x_3 a_3)) &&& eqn(y,x_3)) ||| (exists 5 (\ [i_4,j_4,x_4,y_4,a_4] -> eqn(j,j_4) &&& eqn(a,(cons x_4 a_4)) &&& eqn(y,y_4) &&& elem(i,a_4,y_4) &&& eqn(j_4,(succ i_4))))))) = (exists 5 (\ [i,j,x,y,a] -> ((exists 2 (\ [x_1,a_1] -> eqn(p,a) &&& eqn(q,x) &&& eqn(r,y) &&& eqn(j,succ i) &&& eqn(i,zero) &&& eqn(a,(cons x_1 a_1)) &&& eqn(x,x_1))) ||| (exists 5 (\ [i_2,j_2,x_2,y_2,a_2] -> eqn(p,a) &&& eqn(q,x) &&& eqn(r,y) &&& eqn(j,succ i) &&& eqn(i,j_2) &&& eqn(a,(cons x_2 a_2)) &&& eqn(x,y_2) &&& elem(i_2,a_2,y_2) &&& eqn(j_2,(succ i_2))))) &&& (exists 2 (\ [x_3,a_3] -> eqn(j,zero) &&& eqn(a,(cons x_3 a_3)) &&& eqn(y,x_3))) ||| ((exists 2 (\ [x_1,a_1] -> eqn(p,a) &&& eqn(q,x) &&& eqn(r,y) &&& eqn(j,succ i) &&& eqn(i,zero) &&& eqn(a,(cons x_1 a_1)) &&& eqn(x,x_1))) ||| (exists 5 (\ [i_2,j_2,x_2,y_2,a_2] -> eqn(p,a) &&& eqn(q,x) &&& eqn(r,y) &&& eqn(j,succ i) &&& eqn(i,j_2) &&& eqn(a,(cons x_2 a_2)) &&& eqn(x,y_2) &&& elem(i_2,a_2,y_2) &&& eqn(j_2,(succ i_2))))) &&& (exists 5 (\ [i_4,j_4,x_4,y_4,a_4] -> eqn(j,j_4) &&& eqn(a,(cons x_4 a_4)) &&& eqn(y,y_4) &&& elem(i,a_4,y_4) &&& eqn(j_4,(succ i_4)))))) = (exists 5 (\ [i,j,x,y,a] -> ((exists 4 (\ [x_1,a_1,x_3,a_3] -> eqn(p,a) &&& eqn(q,x) &&& eqn(r,y) &&& eqn(j,succ i) &&& eqn(i,zero) &&& eqn(a,(cons x_1 a_1)) &&& eqn(x,x_1) &&& eqn(j,zero) &&& eqn(a,(cons x_3 a_3)) &&& eqn(y,x_3))) ||| (exists 7 (\ [i_2,j_2,x_2,y_2,a_2,x_3,a_3] -> eqn(p,a) &&& eqn(q,x) &&& eqn(r,y) &&& eqn(j,succ i) &&& eqn(i,j_2) &&& eqn(a,(cons x_2 a_2)) &&& eqn(x,y_2) &&& elem(i_2,a_2,y_2) &&& eqn(j_2,(succ i_2)) &&& eqn(j,zero) &&& eqn(a,(cons x_3 a_3)) &&& eqn(y,x_3))))
63
||| ((exists 7 (\ [x_1,a_1,i_4,j_4,x_4,y_4,a_4] -> eqn(p,a) &&& eqn(q,x) &&& eqn(r,y) &&& eqn(j,succ i) &&& eqn(i,zero) &&& eqn(a,(cons x_1 a_1)) &&& eqn(x,x_1) &&& eqn(j,j_4) &&& eqn(a,(cons x_4 a_4)) &&& eqn(y,y_4) &&& elem(i,a_4,y_4) &&& eqn(j_4,(succ i_4)))) ||| (exists 10 (\ [i_2,j_2,x_2,y_2,a_2,i_4,j_4,x_4,y_4,a_4] -> eqn(p,a) &&& eqn(q,x) &&& eqn(r,y) &&& eqn(j,succ i) &&& eqn(i,j_2) &&& eqn(a,(cons x_2 a_2)) &&& eqn(x,y_2) &&& elem(i_2,a_2,y_2) &&& eqn(j_2,(succ i_2)) &&& eqn(j,j_4) &&& eqn(a,(cons x_4 a_4)) &&& eqn(y,y_4) &&& elem(i,a_4,y_4) &&& eqn(j_4,(succ i_4))))))) = (exists 5 (\ [i,j,x,y,a] -> (exists 4 (\ [x_1,a_1,x_3,a_3] -> eqn(p,a) &&& eqn(q,x) &&& eqn(r,y) &&& eqn(j,succ i) &&& eqn(i,zero) &&& eqn(a,(cons x_1 a_1)) &&& eqn(x,x_1) &&& {\bf eqn(j,zero)} &&& eqn(a,(cons x_3 a_3)) &&& eqn(y,x_3))) ||| (exists 7 (\ [i_2,j_2,x_2,y_2,a_2,x_3,a_3] -> eqn(p,a) &&& eqn(q,x) &&& eqn(r,y) &&& eqn(j,succ i) &&& eqn(i,j_2) &&& eqn(a,(cons x_2 a_2)) &&& eqn(x,y_2) &&& elem(i_2,a_2,x) &&& eqn(j_2,(succ i_2)) &&& eqn(j,zero) &&& eqn(a,(cons x_3 a_3)) &&& eqn(y,x_3))) ||| (exists 7 (\ [x_1,a_1,i_4,j_4,x_4,y_4,a_4] -> eqn(p,a) &&& eqn(q,x) &&& eqn(r,y) &&& eqn(j,succ i) &&& eqn(i,zero) &&& eqn(a,(cons x_1 a_1)) &&& eqn(x,x_1) &&& eqn(j,j_4) &&& eqn(a,(cons x_4 a_4)) &&& eqn(y,y_4) &&& elem(i,a_4,y_4) &&& eqn(j_4,(succ i_4)))) ||| (exists 10 (\ [i_2,j_2,x_2,y_2,a_2,i_4,j_4,x_4,y_4,a_4] -> eqn(p,a) &&& eqn(q,x) &&& eqn(r,y) &&& eqn(j,succ i) &&& eqn(i,j_2) &&& eqn(a,(cons x_2 a_2)) &&& eqn(x,y_2) &&& elem(i_2,a_2,y_2) &&& eqn(j_2,(succ i_2)) &&& eqn(j,j_4) &&& eqn(a,(cons x_4 a_4)) &&& eqn(y,y_4) &&& elem(i,a_4,y_4) &&& eqn(j_4,(succ i_4)))))) = (exists 12 (\ [i,j,x,y,a,x_1,a_1,i_4,j_4,x_4,y_4,a_4] -> eqn(p,a) &&& eqn(q,x) &&& eqn(r,y) &&& eqn(j,succ i) &&& eqn(i,zero) &&& eqn(a,(cons x_1 a_1)) &&& eqn(x,x_1) &&& eqn(j,j_4) &&& eqn(a,(cons x_4 a_4)) &&& eqn(y,y_4) &&& elem(i,a_4,y_4) &&& eqn(j_4,(succ i_4)))) ||| (exists 15 (\ [i,j,x,y,a,i_2,j_2,x_2,y_2,a_2,i_4,j_4,x_4,y_4,a_4] -> eqn(p,a) &&& eqn(q,x) &&& eqn(r,y) &&& eqn(j,succ i) &&& eqn(i,j_2) &&& eqn(a,(cons x_2 a_2)) &&& eqn(x,y_2) &&& elem(i_2,a_2,y_2) &&& eqn(j_2,(succ i_2)) &&&
64
eqn(j,j_4) &&& eqn(a,(cons x_4 a_4)) &&& eqn(y,y_4) &&& elem(i,a_4,y_4) &&& eqn(j_4,(succ i_4)))) = (exists 3 (\ [x,y,a_4] -> eqn(q,x) &&& eqn(r,y) &&& eqn(p,(cons x a_4)) &&& elem(zero,a_4,y))) ||| (exists 6 (\ [x,y,i_2,a_2,x_2,a_2] -> eqn(q,x) &&& eqn(r,y) &&& elem(i_2,a_2,x) &&& eqn(p,(cons x_2 a_2)) &&& elem((succ i_2),a_2,y))) = (exists 3 (\ [x,y,a_4] -> eqn(q,x) &&& eqn(r,y) &&& eqn(p,(cons x a_4)) &&& ((exists 2 (\ [x_5,a_5] -> eqn(zero,zero) &&& eqn(a_4,(cons x_5 a_5)) &&& eqn(y,x_5))) ||| (exists 5 (\ [i_6,j_6,x_6,y_6,a_6] -> eqn(zero,j_6) &&& eqn(a_4,(cons x_6 a_6)) &&& eqn(y,y_6) &&& elem(i_6,a_6,y_6) &&& eqn(j_6,(succ i_6))))))) ||| (exists 4 (\ [x,y,a_2,x_4] -> eqn(q,x) &&& eqn(r,y) &&& eqn(p,(cons x_4 a_2)) &&& (adj2(a_2,x,y)))) = ((exists 5 (\ [x,y,a_4,x_5,a_5] -> eqn(q,x) &&& eqn(r,y) &&& eqn(p,(cons x a_4)) &&& eqn(zero,zero) &&& eqn(a_4,(cons x_5 a_5)) &&& eqn(y,x_5))) ||| (exists 8 (\ [x,y,a_4,i_6,j_6,x_6,y_6,a_6] -> eqn(q,x) &&& eqn(r,y) &&& eqn(p,(cons x a_4)) &&& eqn(zero,j_6) &&& eqn(a_4,(cons x_6 a_6)) &&& eqn(y,y_6) &&& elem(i_6,a_6,y_6) &&& eqn(j_6,(succ i_6))))) ||| (exists 4 (\ [x,y,a_2,x_4] -> eqn(q,x) &&& eqn(r,y) &&& eqn(p,(cons x_4 a_2)) &&& (adj2(a_2,x,y)))) = ((exists 3 (\ [x,y,a_5] -> eqn(q,x) &&& eqn(r,y) &&& eqn(p,(cons x (cons y a_5))))) ||| (exists 4 (\ [x,y,a_2,x_4] -> eqn(q,x) &&& eqn(r,y) &&& eqn(p,(cons x_4 a_2)) &&& (adj2(a_2,x,y))))) = adj2(p,q,r)
65
C.2 The Reversing Example rev1(p,q) = (eqn(p,nil) &&& eqn(q,nil)) ||| (exists 4 (\ [x,a,b,c]-> eqn(p,(cons x a)) &&& eqn(q,c) &&& rev1(a,b) &&& append(b,(cons x nil),c))) = (eqn(p,nil) &&& eqn(q,nil)) ||| (exists 4 (\ [x,a,b,c]-> eqn(p,(cons x a)) &&& eqn(q,c) &&& ((eqn(a,nil) &&& eqn(b,nil)) ||| (exists 4 (\ [x_1,a_1,b_1,c_1]-> eqn(a,(cons x_1 a_1)) &&& eqn(b,c_1) &&& rev1(a_1,b_1) &&& append(b_1,(cons x_1 nil),c_1)))) &&& append(b,(cons x nil),c))) = (eqn(p,nil) &&& eqn(q,nil)) ||| (exists 4 (\ [x,a,b,c] -> eqn(p,(cons x nil)) &&& eqn(q,(cons x nil)) ||| (exists 4 (\ [x_1,a_1,b_1,c_1] -> eqn(p,(cons x (cons x_1 a_1))) &&& eqn(q,c) &&& append(c_1,(cons x nil),c) &&& rev1(a_1,b_1) &&& append(b_1,(cons x_1 nil),c_1))))) = (eqn(p,nil) &&& eqn(q,nil)) ||| (exists 4 (\ [x,a,b,c] -> eqn(p,(cons x nil)) &&& eqn(q,(cons x nil)) ||| (exists 4 (\ [x_1,a_1,b_1,c_1] -> eqn(p,(cons x (cons x_1 a_1))) &&& eqn(q,c) &&& rev1(a_1,b_1) &&& (exists 1 (\ [e] -> append((cons x_1 nil),(cons x nil),e) &&& append(b_1,e,c))))))) = (eqn(p,nil) &&& eqn(q,nil)) ||| (exists 4 (\ [x,a,b,c] -> eqn(p,(cons x nil)) &&& eqn(q,(cons x nil)) |||
66
(exists 4 (\ [x_1,a_1,b_1,c_1,e] -> eqn(p,(cons x (cons x_1 a_1))) &&& eqn(q,c) &&& rev1(a_1,b_1) &&& append((cons x_1 nil),(cons x nil),e) &&& append(b_1,e,c))))) = (eqn(p,nil) &&& eqn(q,nil)) ||| (exists 4 (\ [x,a,b,c] -> eqn(p,(cons x nil)) &&& eqn(q,(cons x nil)) ||| (exists 4 (\ [x_1,a_1,b_1,c_1,e] -> eqn(p,(cons x (cons x_1 a_1))) &&& eqn(q,c) &&& rev1(a_1,b_1) &&& append(b_1,(cons x_1 (cons x nil)),c)))))
rev2(p,q,r) = (exists 2 (\ [a,b] -> (eqn(p,a) &&& eqn(q,b) &&& revapp(a,nil,b)))) = (exists 2 (\ [a,b] -> (eqn(p,a) &&& eqn(q,b) &&& (exists 1 (\ [b] -> (eqn(a,nil) &&& eqn(nil,b) &&& eqn(b,b)))) ||| (exists 4 (\ [x_1,a_1,b_1,c_1] -> (eqn(a,(cons x_1 a_1)) &&& eqn(nil,b_1) &&& eqn(b,c_1) &&& revapp(a_1,(cons x_1 b_1),c_1))))))) = (exists 2 (\ [a,b] -> (exists 1 (\ [b] -> (eqn(p,a) &&& eqn(q,b) &&& eqn(a,nil) &&& eqn(nil,b) &&& eqn(b,b))) ||| (exists 4 (\ [x_1,a_1,b_1,c_1] -> (eqn(p,a) &&& eqn(q,b) &&& eqn(a,(cons x_1 a_1)) &&& eqn(nil,b_1) &&& eqn(b,c_1) &&& revapp(a_1,(cons x_1 b_1),c_1))))))) = (exists 2 (\ [a,b] -> (eqn(p,nil) &&& eqn(q,nil)) ||| (exists 4 (\ [x_1,a_1,b_1,c_1] -> (eqn(p,(cons x_1 a_1)) &&& eqn(q,c_1) &&& revapp(a_1,(cons x_1 nil),c_1)))))) = (exists 2 (\ [a,b] ->
67
(eqn(p,nil) &&& eqn(q,nil)) ||| (exists 4 (\ [x_1,a_1,b_1,c_1] -> (eqn(p,(cons x_1 a_1)) &&& eqn(q,c_1) &&& ((exists 1 (\ [b_2] -> (eqn(a_1,nil) &&& eqn((cons x_1 nil),b_2) &&& eqn(c_1,b_2)))) ||| (exists 4 (\ [x_3,a_3,b_3,c_3] -> (eqn(a_1,(cons x_3 a_3)) &&& eqn((cons x_1 nil),b_3) &&& eqn(c_1,c_3) &&& revapp(a_3,(cons x_3 b_3),c_3)))))))))) = (eqn(p,nil) &&& eqn(q,nil)) ||| (exists 4 (\ [x_1,a_1,b_1,c_1] -> (exists 1 (\ [b_2] -> (eqn(p,(cons x_1 a_1)) &&& eqn(q,c_1) &&& eqn(a_1,nil) &&& eqn((cons x_1 nil),b_2) &&& eqn(c_1,b_2)))) ||| (exists 4 (\ [x_3,a_3,b_3,c_3] -> (eqn(p,(cons x_1 a_1)) &&& eqn(q,c_1) &&& eqn(a_1,(cons x_3 a_3)) &&& eqn((cons x_1 nil),b_3) &&& eqn(c_1,c_3) &&& revapp(a_3,(cons x_3 b_3),c_3)))))) = (eqn(p,nil) &&& eqn(q,nil)) ||| (exists 4 (\ [x_1,a_1,b_1,c_1] -> (eqn(p,(cons x_1 nil)) &&& eqn(q,(cons x_1 nil))) ||| (exists 4 (\ [x_3,a_3,b_3,c_3] -> (eqn(p,(cons x_1 (cons x_3 a_3))) &&& eqn(q,c_3) &&& revapp(a_3,(cons x_3 (cons x_1 nil)),c_3)))))) = (eqn(p,nil) &&& eqn(q,nil)) ||| (exists 4 (\ [x_1,a_1,b_1,c_1] -> (eqn(p,(cons x_1 nil)) &&& eqn(q,(cons x_1 nil))) ||| (exists 4 (\ [x_3,a_3,b_3,c_3] -> (eqn(p,(cons x_1 (cons x_3 a_3))) &&& eqn(q,c_3) &&& (exists 4 (\ [a_4,b_4,c_4,d_4] -> (eqn(a_3,a_4) &&& eqn(c_3,d_4) &&& eqn((cons x_3 (cons x_1 nil)),c_4) &&& rev(a_4,b_4) &&& append(b_4,c_4,d_4))))))))) = (eqn(p,nil) &&& eqn(q,nil)) ||| (exists 4 (\ [x_1,a_1,b_1,c_1] ->
68
(eqn(p,(cons x_1 nil)) &&& eqn(q,(cons x_1 nil))) ||| (exists 4 (\ [x_3,a_3,b_3,c_3] -> (exists 4 (\ [a_4,b_4,c_4,d_4] -> (eqn(p,(cons x_1 (cons x_3 a_4))) &&& eqn(q,d_4) &&& rev(a_4,b_4) &&& append(b_4,(cons x_3 (cons x_1 nil)),d_4))))))))
69
Bibliography [AdBR93] K.R. Apt, J.W. de Bakker, and J.J.M.M. Rutten, editors. Logic Programming Languages. MIT Press, 1993. [AE82] K.R. Apt and M.H. Emden. Contributions to the theory of logic programming. Journal of the ACM, 29(3):841{862, July 1982. [Ant91] S. Antoy. Lazy evaluation in logic. In PLILP 91, Passau, Germany, volume 528 of Lecture Notes in Computer Science, pages 371{382. Springer-Verlag, 1991. [Apt97] K.R. Apt. From Logic Programming to Prolog. Prentice Hall, 1997. [BdM97] R. Bird and O. de Moor. Algebra of Programming. Prentice Hall, 1997. [BL86] M. Bellia and G. Levi. The relation between logic and functional languages: a survey. Journal of Logic Programming, 3(3):317{ 236, 1986. [BLR94] E. Borger, F. J. Lopez-Fraguas, and M. Rodriguez-Artalejo. A model for mathematical analysis of functional logic programs and their implementations. In B. Pehrson and I. Simon, editors, IFIP 13th World Computer Congress, volume I: Technology/Foundations, pages 410{415, 1994. [Bor94] E. Borger. Logic programming: The evolving algebra approach. In B. Pehrson and I. Simon, editors, IFIP 13th World Computer Congress, volume I: Technology/Foundations, pages 391{395, Elsevier, Amsterdam, the Netherlands, 1994. 70
[BR91]
E. Borger and D. Rosenzweig. A formal speci cation of Prolog by tree algebras. In V. C eric, V. Dobric, V. Luzar, and R. Paul, editors, Information Technology Interfaces, pages 513{518. University Computing Center, Zagreb, Zagreb, 1991. [BR94] E. Borger and E. Riccobene. Logic + Control revisited: an abstract interpreter for Godel programs. In G. Levi, editor, Advances in Logic Programming Theory. Oxford University Press, 1994. [BW88] R. Bird and P. Wadler. Introduction to Functional Programming. Prentice Hall, 1988. [CKW93] W. Chen, M. Kifer, and D.S. Warren. Hi Log: A foundation for higher-order logic programming. Journal of Logic Programming, 15(3):187{230, 1993. [CT82] K.L. Clark and S.-A. Tarnlund, editors. Logic Programming. Number 16 in A.P.I.C Studies in Data Processing. Academic Press, 1982. [DFP86] J. Darlington, A. J. Field, and H. Pull. The uni cation of functional and logic languages. In D. DeGroot and G. Lindstrom, editors, Logic Programming: Functions, Relations and Equations, pages 37{70. Prentice Hall, 1986. [DGP92] J. Darlington, Y. Guo, and H. Pull. A new perspective on integrating functional and logic languages. In Proceedings of FGCS, pages 682{693, 1992. [Ede85] E. Eder. Properties of substitutions and uni cations. Journal of Symbolic Computation, 1:31{46, 1985. [FWH92] D.P. Friedman, M. Wand, and C.T. Haynes, editors. Essentials of Programming Languages. MIT Press, 1992. [GH95] T.S. Gegg-Harrison. Representing logic program schemata in Prolog. In Leon Sterling, editor, Proceedings of the Twelfth International Conference on Logic Programming, pages 467{481, Kanagawa, Japan, June 1995. [GLMP91] E. Giovanetti, G. Levi, C. Moiso, and C. Palamidessi. KernelLEAF: A logic plus functional language. Journal of Computer and System Sciences, 42(2):139{185, April 1991. 71
[GM87]
J.A. Goguen and J. Meseguer. Models and equality for logical programming. In Proceedings, TAPSOFT87, volume 250 of Lecture Notes in Computer Science, pages 1{22. Springer Verlag, 1987. [Han94] M. Hanus. The integration of functions into logic programming: From theory to practice. Journal of Logic Programming, 19(20):583{628, 1994. [Han97] M. Hanus. A uni ed computation model for functional and logic programming. In Proc. of the 24th Annual SIGPLANSIGACT Symposium on Principles of Programming Languages (POPL'97), pages 80{93, 1997. [HFN96] A. Hamfelt and J. Fischer Nilsson. Declarative logic programming with primitive recursive relations on lists. In M. Maher, editor, Proceedings of the Joint International Conference and Symposium on Logic Programming, pages 230{243. MIT Press, 1996. [HJ98] C.A.R. Hoare and H. Jifeng. Unifying Theories of Programming. Prentice Hall, 1998. [HKMN95] M. Hanus, H. Kuchen, and J.J. Moreno-Navarro. Curry: A truly functional logic language. In Proc. ILPS'95 Workshop on Visions for the Future of Logic Programming, pages 95{107, 1995. [Hoa85] C.A.R. Hoare. Communicating Sequential Processes. Prentice Hall, 1985. [Hog81] C.J. Hogger. Derivation of logic programs. Journal of the ACM, 28(2):372{392, April 1981. [Hog90] C.J. Hogger, editor. Essentials of Logic Programming. Clarendon Press, 1990. [JM84] N.D. Jones and A. Mycroft. Stepwise development of operational and denotational semantics for Prolog. IEEE, 1984. [Llo93] J.W. Lloyd. Foundations of Logic Programming. Springer Verlag, 1993. 72
[Llo94]
J. W. Lloyd. Combining functional and logic programing languages. In M. Bruynooghe, editor, Proc. Eleventh International Logic Programming Symposium, 1994. [Llo95] J.W. Lloyd. Declarative programming in Escher. Technical Report CSTR-95-013, Department of Computer Science, University of Bristol, June 1995. [Llo98] J.W. Lloyd. Programming in an integrated functional and logic language. The Journal of Functional and Logic Programming, 1998. to appear. [Lut89] S. Luttringhaus. An interpreter with lazy evaluation for PROLOG with functions. In Proceedings of the 2nd Workshop on Computer Science Logic, volume 385 of Lecture Notes in Computer Science, pages 199{225, Berlin, October 1989. SpringerVerlag. [McP98] R. McPhee. Compositional Logic Programming. PhD thesis, Oxford University Computing Laboratory, (to be submitted) December 1998. [Md96] R. McPhee and O. de Moor. Compositional logic programming. In Proceedings of the JICSLP'96 post-conference workshop: Multi-paradigm logic programming, Report 96-28. Technische Universitat Berlin, 1996. [Mil91] D. Miller. A logic programming language with lambdaabstraction, function variables, and simple uni cation. Journal of Logic and Computation, 1(4):497{536, 1991. [MNRA92] J. Moreno-Navarro and M. Roderiguez-Artalejo. Logic programming with functions and predicates: The language Babel. Journal of Logic Programming, 12(3):191{223, 1992. [Nai96] L. Naish. Higher-order logic programming in Prolog. In M. Chakravarty, Y. Guo, and T. Ida, editors, Proceedings of the JICSLP'96 post-conference workshop: multi-paradigm logic programming, Report 96-28. Technische Universitat Berlin, 1996. [Nar86] S. Narain. A technique for doing lazy evaluation in Prolog. Journal of Logic Programming, 3(3):259{276, 1986. 73
[NM88]
[Pau85] [PP91]
[Red87] [Rob65] [Rob88] [Ros98] [Ros00] [RS82] [Sha89] [SHC95]
G. Nadathur and D. Miller. An overview of Prolog. In R. Kowalski and K. A. Bowen, editors, Fifth International Logic Programming Conference, pages 810{827, Seattle, U.S.A., 1988. MIT Press. L.C. Paulson. Lessons learned from LCF: a survey of natural deduction proofs. Computer Journal, pages 474{479, 1985. M. Proietti and A. Pettorossi. Semantics preserving transformation rules for Prolog. In Proceedings of PEPM91 (ACM Symposium on Partial Evaulation and Semantics Based Program Manipulation), volume 26 of Sigplan Notices, pages 274{284. ACM, September 1991. U.S. Reddy. Functional logic languages part I. In Proceedings of a Workshop on Graph Reduction, volume 279 of Lecture Notes in Computer Science, pages 401{425. Springer-Verlag, 1987. J.A. Robinson. A machine-oriented logic based on the resolution principle. Journal of the ACM, 12(1):23{41, January 1965. J.A. Robinson. Beyond LogLisp: combining functional and relational programming in a reduction setting. Machine intelligence, 11:57{68, 1988. A.W. Roscoe. The Theory and Practice of Concurrency. Prentice Hall, 1998. B.J. Ross. Using algebraic semantics for proving Prolog termination and transformation. don't know, pages 62{70, 2000. J.A. Robinson and E.E. Sibert. LogLisp: An alternative to Prolog. Machine Intelligence, 10:399{419, 1982. E. Shapiro. The family of concurrent logic programming languages. ACM Computing Surveys, 21(3):413{510, September 1989. Z. Somogyi, F.J. Henderson, and T. Conway. The implementation of Mercury: an ecient purely declarative logic programming language. In Proceedings of the Australian Computer Science Conference, pages 127{ 140, Glenelg, Australia, February 1995. available at http://www.cs.mu.oz.au/mercury/papers.html. 74
[Smo84] [Spi96] [SS86] [vEK76] [Wan80] [WP00]
G. Smolka. Making control and data ow in logic programs explicit. Journal of the ACM, 8, 1984. J.M. Spivey. An Introduction to Logic Programming through Prolog. Prentice Hall, 1996. L. Sterling and E. Shapiro. The Art of Prolog. MIT Press, 1986. M.H. van Emden and R.A. Kowalski. The semantics of predicate logic as a programming language. Journal of the ACM, 23(4):722{742, October 1976. M. Wand. Continuation-based program transformation strategies. Journal of the ACM, 27(1):164{180, January 1980. D.H.D. Warren and L.M. Pereira. Prolog { the language and its implementation compared with Lisp. Technical report, Department of Arti cial Intelligence, University of Edinburgh, Scotland, 2000.
75