Academia.eduAcademia.edu

Prolog for Imperative Programmers

2002, Consortium for Computing Sciences in Colleges

Once students master the imperative programming paradigm, they find it hard to learn to work with a declarative language such as Prolog. In order to help our students make the transition from imperative to pure declarative programming, we have identified many common misconceptions and pitfalls to which imperative programmers are susceptible. We have attempted to recast the semantics of commonly misunderstood data and control constructs in Prolog in terms of the more familiar imperative constructs. Furthermore, in order to promote declarative rather than imperative programming in Prolog, we have devised a set of programming templates, organized by input-output characteristics of the problems to which they apply. We have found that these templates are helpful in teaching logic programming in our Comparative Programming Languages course. In this paper, we will present the common misconceptions of imperative programmers as they begin programming in Prolog, and our attempts to dispel them. We will also present and analyze the templates we have developed to help our students make the transition from imperative to declarative programming. Finally, we will list some of the problems that our students have successfully solved over the years, as evidence of the effectiveness of the tools and techniques we present. factorial(Number, Factorial):-NewNumber is Number-1, factorial(NewNumber, Factorial), Factorial is Number * Factorial. Note that the same variable Factorial is used to save both the value of Number! and (Number-1)! Needless to say, this code fails to work. • The behavior of the matching algorithm confounds the understanding of imperative programmers who are already unclear about the distinction between assignment and instantiation. When an uninstantiated variable is matched with a constant, the variable is instantiated to the constant. However, when an instantiated variable is matched with a constant, the variable is compared with the constant, returning true if the two values are the same, and false otherwise. The following clause, written once too many times by imperative programmers, illustrates this point: Number is Number-1 Whereas imperative programmers expect Number to be assigned the value of Number-1, instead, the value of Number is compared with Number-1, a This refers to the simple case when all the three sub-goals can be satisfied without any backtracking. If backtracking is involved, the equivalent imperative code would be: while(subgoal-One()) while(subgoal-Two()) if(subgoal-Three()) return true;

PROLOG FOR IMPERATIVE PROGRAMMERS Amruth N Kumar Ramapo College of New Jersey 505 Ramapo Valley Road Mahwah, NJ 07430-1680 [email protected] ABSTRACT Once students master the imperative programming paradigm, they find it hard to learn to work with a declarative language such as Prolog. In order to help our students make the transition from imperative to pure declarative programming, we have identified many common misconceptions and pitfalls to which imperative programmers are susceptible. We have attempted to recast the semantics of commonly misunderstood data and control constructs in Prolog in terms of the more familiar imperative constructs. Furthermore, in order to promote declarative rather than imperative programming in Prolog, we have devised a set of programming templates, organized by input-output characteristics of the problems to which they apply. We have found that these templates are helpful in teaching logic programming in our Comparative Programming Languages course. In this paper, we will present the common misconceptions of imperative programmers as they begin programming in Prolog, and our attempts to dispel them. We will also present and analyze the templates we have developed to help our students make the transition from imperative to declarative programming. Finally, we will list some of the problems that our students have successfully solved over the years, as evidence of the effectiveness of the tools and techniques we present. 1 INTRODUCTION In most schools, Computer Science students begin programming in an imperative programming language. Many of them find it very hard to learn a declarative language such as Prolog after getting into an imperative mind-set. We have identified common misconceptions and pitfalls to which imperative programmers are susceptible as they begin to learn Prolog. In order to counter these misconceptions, we have recast the semantics of Prolog constructs in terms of imperative constructs. Often, when imperative programmers learn Prolog, they tend to write imperative rather than declarative code in Prolog syntax, using assert()s and retract()s. In order to introduce declarative rather than imperative programming in Prolog, we have devised a set of templates. We have arranged these templates according to the characteristics of the problems to which they are applicable, an arrangement that helps students recall and reuse the right template in a programming situation. We have used these templates to teach Prolog in our Comparative Programming Languages course for several years. We have found that teaching with templates expedites the instruction of Prolog, while at the same time providing students with a better understanding of declarative programming in Prolog. In this paper, we will present and analyze several of these templates. In Section 2, we will present some common misconceptions among imperative programmers about the data types and variables in Prolog. In Section 3, we will recast the semantics of Prolog constructs in terms of imperative constructs. We will present and analyze several templates for writing declarative programs in Prolog in Section 4. Finally, in Section 5, we will list some of the problems our students in Comparative Programming Languages course have successfully solved in pure Prolog using the material we present in this paper. 2 THE DATA TYPES AND DATA VARIABLES One of the stumbling blocks for imperative programmers learning Prolog is variables. Variables in Prolog are different in many ways from their counterparts in imperative languages: • Variables in Prolog are not typed. Therefore, we need not “declare” variables before using them. Imperative programmers are often so wedded to the concept of declaring variables before using them (a good/required practice in imperative programming) that unless they are disabused of this notion for Prolog, they are reluctant to “create” new variables “on the fly”. This leads them to write incorrect code as described next. • Variables in Prolog cannot be assigned, but only instantiated. Once a variable is instantiated to a value, the only way one can change its value is by un-instantiating it first through backtracking. Imperative programmers, who assume that instantiation and assignment are one and the same tend to write code such as: factorial( Number, Factorial ):NewNumber is Number - 1, factorial( NewNumber, Factorial ), Factorial is Number * Factorial. • Note that the same variable Factorial is used to save both the value of Number! and (Number – 1)! Needless to say, this code fails to work. The behavior of the matching algorithm confounds the understanding of imperative programmers who are already unclear about the distinction between assignment and instantiation. When an uninstantiated variable is matched with a constant, the variable is instantiated to the constant. However, when an instantiated variable is matched with a constant, the variable is compared with the constant, returning true if the two values are the same, and false otherwise. The following clause, written once too many times by imperative programmers, illustrates this point: Number is Number – 1 Whereas imperative programmers expect Number to be assigned the value of Number – 1, instead, the value of Number is compared with Number – 1, a comparison that inevitably fails. So, even though the code does not generate a syntax error, it does not seem to work “as intended”, leaving imperative programmers quite puzzled. Therefore, we encourage our students to never reuse variables, but rather to generously create and use new variables to hold the intermediate results. We encourage our students to limit themselves to the purely declarative subset of Prolog [1] when they write programs. In this spirit, we encourage them to use the list as the universal data structure. This is a recursive data structure that permits only sequential access. Although imperative programmers feel constrained by its lack of random access (a la’ arrays), they quickly realize that when combined with untyped variables, the list data type subsumes both the array and the structure types of imperative programming languages. 3 THE CONTROL CONSTRUCTS Teaching in terms of what students already know is a standard practice in educational circles. In this spirit, we present Prolog in terms of the control constructs found in all imperative programming languages: sequence, selection, repetition and abstraction Sequence: The equivalent of statements in imperative languages is clauses in Prolog. The delimiter for clauses is a period. Just as statements in imperative languages are executed top to bottom, clauses in Prolog databases are tried top to bottom, and literals in a goal are attempted left to right. Even though the literals in a Prolog goal are executed left to right, they are not executed sequentially, but rather conditionally. E.g. consider the following goal: ?- subgoal-One(), subgoal-Two(), subgoal-Three(). On the face of it, imperative programmers assume that the above goal is equivalent to the following sequence of imperative statements: subgoal-One(); subgoal-Two(); subgoal-Three(); In fact, the Prolog goal is executed as: if( subgoal-One() ) if( subgoal-Two() ) if( subgoal-Three() ) return true; This refers to the simple case when all the three sub-goals can be satisfied without any backtracking. If backtracking is involved, the equivalent imperative code would be: while( subgoal-One() ) while( subgoal-Two() ) if( subgoal-Three() ) return true; If more than one answer is desired for the goal, the equivalent imperative code would now be: while( subgoal-One() ) while( subgoal-Two() ) while( subgoal-Three() ) if( enough answers found ) return true; The above statement is simply a procedural description of the depth-first behavior of the inference engine of Prolog. However, we find that casting this behavior in terms of the execution of imperative statements is helpful to imperative programmers. Selection: Two/Multi-way selection in Prolog is implemented simply by writing a separate clause for each case. The clauses are listed in the database in the order in which they should be tried in the program. E.g., the C++ code to determine the letter grade corresponding to a numerical grade is: if( grade >= 90 ) letter_grade = else if( grade >= letter_grade = else if( grade >= letter_grade = else if( grade >= letter_grade = else letter_grade = ‘A’; 80 ) ‘B’; 70 ) ‘C’; 55 ) ‘D’; ‘F’; The corresponding Prolog code would consist of five distinct clauses in the database: grade( grade( grade( grade( grade( Number, Letter ):- Number Number, Letter ):- Number Number, Letter ):- Number Number, Letter ):- Number _, Letter ):- Letter = f. >= >= >= >= 90, 80, 70, 55, Letter Letter Letter Letter = = = = a. b. c. d. Imperative programmers find it unusual that these five clauses are not “syntactically encapsulated” – something they are used to expect in imperative languages. Therefore, we find it necessary to emphasize that these five distinct clauses are together semantically equivalent to the single nested if-else statement in C++. Anonymous variables are another concept with which imperative programmers are unfamiliar. This example illustrates the use of an anonymous variable – if the execution reaches the last clause, we want the goal to match the clause regardless of the value of the numerical grade. Repetition: Recursion is used to implement repetition in Prolog. A recursive definition includes one or more base cases and one or more recursive cases. Base cases are usually facts, and recursive cases are rules in the database. Each case is a separate clause, and the order in which they are listed in the database is important to the correctness of the program: base clauses must be listed before recursive clauses. We will consider several recursive examples in the next section in the context of input/output templates. We may also use backtracking with fail to implement repetition, especially when we want to generate all the possible answers to a query. E.g., if a library lists all its books in a database using the predicate holding(author, title, year), the following goal would repeatedly print the titles of all the books acquired in 2001: ?- holding( _, Title, 2001), write( Title ), nl, fail. Abstraction: The equivalent of functions in imperative languages are clauses: facts and rules: • Facts are trivial functions. They accomplish their purpose through matching of arguments. • Rules are equivalent to the traditional functions in imperative languages, with a header (head) and a body. There are some significant differences between functions in imperative languages and rules in Prolog: o Since variables are not typed, programmers should feel free to generously use new variables while writing the body of a rule, without having to declare them in advance as they would in an imperative language. We have noticed that without this explicit encouragement, students tend to “reuse” existing variables and abstain from creating new ones, which results in incorrect code as described earlier. o Due to the nature of the variables and the matching algorithm, parameters in Prolog clauses are passed either by value (copied from the goal’s argument to a database clause’s argument at the time the goal is matched with the database clause) or by result (copied from a database clause’s argument to the goal’s argument at the time the sub-goals generated by the database clause are satisfied), and never by reference or value-result (See Table 1). Copy semantics is always used for parameter passing. • Each goal literal is a function call. Therefore, the body of a rule is nothing but a series of function calls, and their execution is conditional rather than sequential as described under the Sequence section. Since variables are not typed, Prolog clauses naturally implement the polymorphic behavior of templates in imperative languages. Not only does template polymorphism come naturally to Prolog, the fact that it predates the inclusion of template polymorphism in C++ can be used to give students an appreciation for the importance of knowing the history of computing and the evolution of languages. Prolog clauses permit the equivalent of “function overloading” in imperative languages. We can write multiple clauses for a given predicate: clauses with the same signature are considered to be alternatives, whereas clauses with differing signatures are considered to be unrelated. Pure Prolog does not permit global variables. The boolean value returned by individual clauses controls only the conditional execution of the Prolog program, as described earlier in the Sequence section. So, all the results of computation in a Prolog program must be communicated among the clauses through their arguments, i.e., “parameters”. Imperative programmers must realize this before they can write any large Prolog programs consisting of multiple clauses. In Table 1, we summarize the behavior of scalar variables used as parameters. In Table 2, we present the behavior of list variables used as parameters. Note that when using list variables as parameters, the parameters themselves may be used for elementary list operations such as accessing elements and constructing a list from its elements – we do not have to introduce any additional literals in a clause to do these jobs. Argument in Goal Literal (Actual Parameter) Argument in Database Clause (Formal Significance Parameter) Uninstantiated Variable Uninstantiated Variable Aliasing Uninstantiated Variable Instantiated Variable Pass By Result Instantiated Variable Uninstantiated Variable Pass By Value Instantiated Variable Instantiated Variable Comparison for Equality Table 1: Matching Scalar Arguments of a Goal Literal with those of a Database Clause Argument in Goal Literal (Actual Parameter) [ H | T ] Argument in Database Clause Significance Uninstantiated Variable Uninstantiated Variables H, T Aliasing Uninstantiated Variable Instantiated Variables H, T List Construction Instantiated Variable Uninstantiated Variables H, T List Access Instantiated Variable Instantiated Variables H, T List Comparison Table 2: Matching Vector Arguments of a Goal Literal with those of a Database Clause 4 THE TEMPLATES We have devised a set of templates to help our students make the transition from imperative programming to declarative programming in Prolog. These templates present idiomatic Prolog clauses for typical applications. We have organized the templates in terms of the input-output characteristics of the problems they can be used to solve. This organization helps students recall and reuse the right template for a given problem. We will discuss our templates for the following input-output combinations: • Scalar Input o Output by Side-Effect o Scalar Output o Vector Output • Vector Input o Output by Side-Effect o Scalar Output o Vector Output Note that scalars are terms (constants and variables) and vectors are lists. Side-effect is through read() and write() clauses. In our Comparative Programming Languages course, we use similar templates to also teach LISP [3], thereby enabling our students to compare and contrast logic-based programming with functional programming. We will now present each template with an example. Since these templates present recursive solutions, we present them in terms of base case(s) and recursive case(s). In the recursive case, we differentiate between local action, i.e., the work done locally within a clause, and the work done by the recursive call. The generalized structure of the templates is: base-case(). recursive-case():- local-action(), recursive-call(). 4.1 Scalar Input, Output by Side-Effect Problem: Write a program to print the squares of all the numbers from a given number down to 1. % Base Case – Stop printing when Number is 0 printSquares( 0 ). % Recursive Case – Print Number, call recursively on Number - 1 printSquares( Number ) :- Square is Number * Number, write( Square ), nl, NewNumber is Number - 1, printSquares( NewNumber ). Analysis: We begin with this example to illustrate how the base case and recursive case are two distinct clauses, listed in that order in the database. The base case is a fact, and the recursive case is a rule. The scalar input is passed as an argument to the clauses. The clause for the recursive case changes the value of the input Number in the direction of its base case value, viz., 0. If the database is queried with a negative number, the result would be infinite recursion! This could be used as a lead-in to discuss making end conditions robust. In general, the weakest end condition(s) must be used. Whereas the above end condition tests for Number being 0, a more robust design would test for negative numbers as well. This could be done simply by changing the base case into a rule as follows: % Base Case – Stop printing when Number is 0 or less printSquares( Number ):- Number =< 0. This example also illustrates the concept of tail recursion, where the recursive call is the last literal in the body of the recursive case. During tail recursion, all the local action in a clause is done before the recursive call, and none after returning from the recursive call, as may be clarified using a recursion tree. Imperative programmers find it interesting to note that any clause with tail recursion can be rewritten as a loop. This also drives home the more general point that recursion is just an alternative to iteration: it demystifies recursion for imperative programmers. In order to illustrate head recursion, where the recursive call precedes all local action in a recursive case, we will consider the next problem. Problem: Modify the above program to print the squares of all the numbers from 1 up to a specified number. % Base Case – Stop printing when Number is 0 or less printSquares( Number ):- Number =< 0. % Recursive Case – Call recursively on Number–1, then print Number printSquares( Number ) :- NewNumber is Number - 1, printSquares( NewNumber ), Square is Number * Number, write( Square ), nl. Analysis: The only difference between this and the previous code is that the recursive call occurs before the local action in the recursive case. This is an example of head recursion, where all the local action is done after returning from the recursive call. It is enough to reorder the literals in the body of the recursive rule to reverse the order in which the squares are printed! An extension of this discussion might be to consider the rule wherein the recursive call is performed both before and after the local action in the body of the rule. Therefore, the squares would be printed first in descending order, followed by ascending order. 4.2 Scalar Input, Scalar Output Problem: Write a program to calculate the factorial of a number. % Base Case – Factorial of 0 is 1. factorial( 0, 1 ). % Recursive Case – N! = N * (N – 1)! factorial( Number, Factorial ):NewNumber is Number - 1, factorial( NewNumber, NewFactorial ), Factorial is Number * NewFactorial. Analysis: Since the output is a scalar, and parameters are the only means through which Prolog clauses can communicate results with each other, we need a second argument for the scalar output. Typically, the input argument is passed by value and the output argument is passed by result. In the recursive case, the result obtained from the recursive call is composed with the input to calculate the output. Note that the base case returns the identity element for the operation used in the recursive case to compose the result of the recursive call with the input, e.g., 1 for multiplication, 0 for addition. 4.3 Scalar Input, Vector Output Problem: Write a program to return the list of factorials of all the numbers from a given number down to 0. % Base Case – If the Number is negative, return a null list listFactorials( Number, [] ):- Number < 0. % Recursive case – Insert the factorial of the current number % as the head of the list returned by the recursive call listFactorials( Number, Vector ):NewNumber is Number - 1, listFactorials( NewNumber, NewVector ), factorial( Number, Factorial ), Vector = [ Factorial | NewVector ]. Analysis: Since the output is a vector, we need a second argument for the vector output. In the recursive case, the result of the local action is inserted as the head and the result of the recursive call is inserted as the tail of a new list – illustrating a classic technique of list construction used in Prolog. In the base case, the null list is returned, which is the identity element for list construction. Whenever the body of a Prolog rule contains an equality predicate with a formal parameter on one side, the predicate can be eliminated in the body and the formal parameter in the head of the rule can be replaced with the other side of the equality predicate. E.g., in the recursive case above, the formal parameter Vector can be replaced by [ Factorial | NewVector ], and the equality predicate eliminated from the body as follows: listFactorials( Number, [ Factorial | NewVector ] ):NewNumber is Number - 1, listFactorials( NewNumber, NewVector ), factorial( Number, Factorial ). Although novice programmers find this substitution confusing at first, once they understand how work is done in Prolog programs during the matching of arguments (e.g., see Tables 1 and 2), they adopt this style. (For a procedural interpretation of this substitution, please see [2], page 2078.) What if we wanted to generate the list of factorials in ascending rather than descending order? Simply exchanging the head and tail during list construction as follows will not produce the desired result: listFactorials( Number, [ NewVector | Factorial ] ):- … Since Factorial is not itself a list, we will end up with “dotted pairs”. Enclosing Factorial in list notation will not solve the problem either: listFactorials( Number, [ NewVector | [ Factorial ] ] ):- … We will end up with a list of nested lists rather than a simple list. The solution is to use append() predicate to append the result of the recursive call to the list of the result of the local action: listFactorials( Number, Vector ):NewNumber is Number - 1, listFactorials( NewNumber, NewVector ), factorial( Number, Factorial ), append( NewVector, [ Factorial ], Vector ). This example illustrates another classic technique of list construction in Prolog. 4.4 Vector Input, Output by Side-Effect Problem: Write a program to print the elements of a list, in order. % Base Case – Stop when the list is empty printElements( [] ). % Recursive Case – Print the first element, % Call recursively on the rest printElements( [ First | Rest ] ):write( First ), nl, printElements( Rest ). Analysis: Since the input is a vector, the base case checks to see if the input is an empty list. In the recursive case, the local action is performed on the head of the list, and the tail of the list is handed to the recursive call. These characteristics are common to all the templates for vector input that follow. The above template is once again an example of tail recursion. By using head recursion, we can reverse the order in which the elements of the list are printed. This involves reversing the order of the local action (write( First )) and the recursive call in the body of the recursive rule. If we want to print the factorials of the elements of the input list, we simply insert the factorial() predicate in the body of the recursive rule, as follows: printElements( [ First | Rest ] ):factorial( First, FirstFactorial ), write( FirstFactorial ), nl, printElements( Rest ). This illustrates the ease with which PROLOG clauses can be composed to build larger programs. It illustrates why Prolog is well suited for rapid prototyping applications. 4.5 Vector Input, Scalar Output Problem: Write a program to obtain the sum of all the elements of a list. % Base case – If the list is empty, the sum is 0 addElements( [], 0 ). % Recursive Case – Add the first element to the partial sum returned % by the recursive call on the rest of the list addElements( [ First | Rest ], Sum ):addElements( Rest, NewSum ), Sum is First + NewSum. Analysis: All the features that were introduced in the last template have been carried over to this template. Note that the base case returns the identity element (0) for the operation (+) used in the recursive case to compose the result of the local action with the result of the recursive call. In the recursive case, the local action is performed on the head of the list and the tail of the list is handed to the recursive call. 4.6 Vector Input, Vector Output Problem: Given a list of numbers, return a list of their respective squares. % Base Case – If the list is empty, return an empty output list squareElements( [], [] ). % Recursive Case – Insert the square of the first element % as the head of the list returned by the recursive call squareElements( [ First | Rest ], [ NewFirst | NewRest ] ):NewFirst is First * First, squareElements( Rest, NewRest ). Analysis: Since the output is a vector, the base case returns the identity element for list construction, viz., []. In the recursive case, the result of the local action is inserted as the head and the result of the recursive call is inserted as the tail of the list that is returned. Just as in the case of the Scalar Input, Vector Output template, we can reverse the order of the output list by using the append() predicate. In some problems, we may want to process only selected elements of the input list. E.g., given a list of numbers, we may want to return a list of only those numbers that are greater than or equal to a threshold value. We can implement this by introducing a second recursive case in which the result of the local action is not composed with the result of the recursive call. % Base Case – If the list is empty, return an empty output list filterList( [], [], _ ). % Recursive Case – If the head of the list is % greater than or equal to the threshold, % insert it as the head of the list returned by the recursive call filterList( [ First | Rest ], [ First | NewRest ], Threshold ):First >= Threshold, filterList( Rest, NewRest, Threshold ). % Recursive Case– If the head of the list is less than the threshold, % simply return the result of the recursive call. filterList( [ _ | Rest ], NewRest, Threshold ):filterList( Rest, NewRest, Threshold ). 4.7 Factoring Out Code Consider another vector-input, scalar-output problem: the problem of finding the largest element in a list. Beginning Prolog programmers may attempt a solution that includes multiple recursive calls, and is hence inefficient: % Base case – If the list has only one element, it is the largest findLargest( [ Number ], Number ). % Recursive Cases – Find the largest element in the rest of the list % Compare it with the first element to determine the % largest element of the entire list findLargest( [ First | Rest ], Largest ):findLargest( Rest, RestLargest ), First >= RestLargest, Largest = First. findLargest( [ First | Rest ], Largest ):findLargest( Rest, RestLargest ), First < RestLargest, Largest = RestLargest. After a little reflection we can see that we can eliminate the recursive call in the last clause: % Recursive Cases findLargest( [ First | Rest ], Largest ):findLargest( Rest, RestLargest ), First < RestLargest, Largest = RestLargest. findLargest( [ First | _ ], First ). However, this formulation of the solution is not quite readable. Moreover, if the user attempts to re-satisfy a query, these clauses would generate incorrect results. A better solution would be one that involves factoring out code, a principle that is relevant to all the paradigms of programming, but especially so to declarative programming. In this example, we may “factor out” the code to compare two numbers and determine the larger number, and introduce a new predicate to carry out this task: % Base case – If the list has only one element, it is the largest findLargest( [ Number ], Number ). % Recursive Cases – Get the largest element in the rest of the list % Compare that with the first element to determine the % largest element of the entire list findLargest( [ First | Rest ], Largest ):findLargest( Rest, RestLargest ), getLarger( First, RestLargest, Largest ). % If the first argument is greater than or equal to the second, % it is the larger number getLarger( First, Second, First ):- First >= Second. % Otherwise, the second argument is the larger number getLarger( First, Second, Second ):- First < Second. Again, we could skip the comparison First < Second in the second getLarger() clause, but the clause would produce incorrect results if backtracking occurs. Finally, note that the above clauses will fail if the input list is empty. This is not a bug, but rather correct behavior, since an empty list does not have a largest element. This example illustrates another principle of Prolog programming, which imperative programmers find to be rather unintuitive: if we want a goal to fail for certain inputs, we simply do not handle those cases of input in the clauses in our database! 4.8 Putting the Templates Together Finally, we can use the following algorithm to solve complex problems using the various templates: 1. Perform a top-down decomposition of the problem; 2. Analyze the input-output characteristics of the resulting sub-problems; 3. Use the appropriate template to write the clauses for each sub-problem; 4. Finally, compose these clauses to write the overall program. Problem: Write a program to calculate the average of a list of non-repeating numbers after eliminating the outliers. Solution: • Finding an outlier (maximum or minimum) is a vector input/scalar output problem. • • Eliminating an outlier from a list is a vector input/vector output problem. Calculating the sum of the elements in a list and counting the number of elements in a list are vector input/scalar output problems. Our overall solution might look like this: average( Vector, Average ):% Remove the highest element in the list findLargest( Vector, Maximum ), delete( Vector, Maximum, VectorSansMax ), % Remove the lowest element in the list findSmallest( VectorSansMax, Minimum ), delete( VectorSansMax, Minimum, VectorSansMin ), % Calculate the average of the remaining elements length( VectorSansMin, Count ), addElements( VectorSansMin, Sum ), Average is Sum / Count. It is instructive to trace the flow of data among the literals in the above rule with arrows. Such an analysis would demonstrate the “one-way” nature of the flow of data in Prolog. Tracing the arrows head to tail, and starting with the input parameters in the head of the rule, we can see how data is handed back and forth between the clauses in the body of the rule, until eventually, the output parameters are generated. 5 CONCLUSIONS We have identified common misconceptions and pitfalls to which imperative programmers are susceptible as they begin to learn Prolog. In order to counter these misconceptions, we have recast the semantics of Prolog constructs in terms of imperative constructs. We have also presented a set of templates with which to introduce logic programming in Prolog to imperative programmers. We have arranged these templates according to the characteristics of the problems to which they are applicable, an arrangement that helps students recall and reuse the right template for a given problem. Our experience suggests that the above set of templates is sufficient for writing Prolog programs to solve most problems. Teaching Prolog from the beginning, up to and including these templates takes us about one week of classroom instruction. We have used the templates and the other material presented in this paper to teach Prolog in our Comparative Programming Languages course for several years. Some of the problems our students have solved in this course in pure Prolog include: Battleship / Salvo, Othello / Reversi, Domino Deluxe, Dice games (Pokerdice, Craps), Card games (Spades, Crazy 8, Blackjack), Snakes and Ladders, Stock Market simulation, LOGO and Hare and Tortoise. Our students had three weeks to write these programs, and were programming in Prolog for the first time in their academic career. They were restricted to programming in pure Prolog. We believe that the material we have presented in this paper, when taught in addition to basic Prolog syntax and semantics, helps students quickly make the transition from imperative to declarative programming. REFERENCES [1] Clocksin, W.F. and Mellish, C.S.: Programming in Prolog, Springer Verlag, 1981. [2] Cohen, J.: Logic Programming and Constraint Logic Programming. In The Computer Science and Engineering Handbook, (Ed. A.B. Tucker), CRC Press, Boca Raton, FL, 1997. [3] Kumar, A.: Using Patterns to Teach Recursion in LISP. Proceedings of the Eastern Small College Computing Conference (ESCCC '96), Marywood College, Scranton, PA, 10/25-26/96, 80-84.