/* This is a simple tutor to be used for illustrations in Kurt VanLehn's graduate seminar on intelligent tutuoring systems, November 1999. It uses Strips operators to represent task domain knowledge. The problems can be solved by either backward and forward chaining. The expert model maintains a goal stack for the sake of backwards chaining. Thus, we assumes that there are several kinds of mental actions that change the student's state such as popping a goal, pushing an operator (which causes its preconditions to be pushed as subgoals) and doing an operator application. This tutor assumes that only doing an operator application is also an observable action. That is, the user interface provides no way for the student to indicate goal manpulations. The student modelling challenge is that the student might make several unobservable mental actions (e.g., pushing and popping the stack) before making an observable action. The pedagogical challenge is that when the student is stuck, the tutor should hint a line of reasoning consisting of several unobservable mental actions leading up to an observable action. The tutor is a model tracing tutor of the most limited kind. It can only understand the student's observable actions if the action is one that the expert model would take next. If the student doesn't enter an operator application that the expert would do next, the tutor chooses the next operator application that it would do and tells the student the line of reasoning leading up to it. */ /* ======================== top level: model tracing ===================== */ /* problems are listed below by name, so you can call tutor(shoes) or tutor(shop1) etc. to test the program. The top level function just initializes the problem and the student state, gets the first student entry and calls the main loop. */ tutor(Problem) :- initialize(Problem, InitialStudentState), get_student_entry(Entry), main_loop(Entry, InitialStudentState). /* the main loop tests to see if the student asked to quit. If the student didn't, then it dispatches on the type of entry made by the student and repeats. */ main_loop(i_quit, _) :- write('Bye!'). main_loop(Entry, StudentState) :- dispatch(Entry, StudentState, NextStudentState), wait_for_ok, get_student_entry(NextEntry), !, /* this cut prevents redoing an iteration if the subsequent iteration fails. */ main_loop(NextEntry, NextStudentState). /* We assume that the student's entry is either i_quit, i_am_stuck or an operator application (operator applications are the only kind of actions that are observable). If the operator application is correct, in that the expert can also generate it, we give minimal positive feedback. Otherwise, we generate the next operator application and coach the student to make it. If we can't generate a next operator opplication but the student's operator application can be executed in the current state, then complain but let the student do it. Otherwise, admit that we are confused. The cuts (!) makes this like a COND clause in that control will only go to the next clause if the preceding clause fails. The numbers are cutoff values for depth-first search. */ dispatch(i_am_stuck, StudentState, NextStudentState) :- !, single_step(20, StudentState, NextStudentState, Trace), coach(Trace). dispatch(Entry, StudentState, NextStudentState) :- single_step(20, StudentState, NextStudentState, Trace), last(Trace,do(Entry)), /* the last member of Trace is always an operator application */ !, positive_feedback(Trace). dispatch(_, StudentState, NextStudentState) :- single_step(20, StudentState, NextStudentState, Trace), !, negative_feedback, coach(Trace). dispatch(Entry, ss(Situation,Stack), ss(NextSituation,Stack)) :- fetch_operator(Entry, Preconds, Effects), all_hold(Preconds, Situation), !, tutor_will_follow_student, execute(Situation, Effects, NextSituation). dispatch(_, State, State) :- tutor_is_confused. /* ==================== student modeling =============================== */ /* single step returns in its last argument a sequence of mental actions leading up to an operator application. It also returns the new student state. Although there is little chance of it failing, it is a depth-first search, so we put a cutoff on it, which is passed in as the first argument. */ single_step(DepthCutoff, StudentState, ReturnedState, [Action | Trace]) :- /* remove comment for debugging pstate(StudentState), */ successor(StudentState, Action, NextState), single_step_recurse(DepthCutoff, Action, NextState, ReturnedState, Trace). single_step_recurse(_, do(_), StudentState, StudentState, []) :- !. /* this cut terminates the search at a do(_) action. If you want to assume that operator applications can be done mentally without being observed on the user interface, then remove this cut. */ single_step_recurse(DepthCutoff, _, StudentState, ReturnedState, Trace) :- DepthCutoff > 0, NewDepthCutoff is DepthCutoff - 1, single_step(NewDepthCutoff, StudentState, ReturnedState,Trace). /* ========================= Expert module ================================= */ /* To just see a problem solved, call e.g., demo(shoes2,G) from the top level. It prints a trace of the expert solving that problem. You can generate alternative solutions by entering a semi-colon when you get back to top level. Or you can call demo(shoes2) if you don't want to play with backing up. */ demo(Problem) :- demo(Problem, _). demo(Problem, TopLevelGoals) :- initialize(Problem, InitialStudentState), operator(finish,TopLevelGoals,_), main_demo_loop(1, InitialStudentState,TopLevelGoals). main_demo_loop(_, ss(Situation,_), TopLevelGoals) :- all_hold(TopLevelGoals, Situation). main_demo_loop(Cycle, State, Goals) :- pstate(State), successor(State, MentalAction, NextState), write(Cycle), write('. Doing: '), write(MentalAction), nl, NextCycle is Cycle + 1, main_demo_loop(NextCycle, NextState, Goals). main_demo_loop(Cycle, _, _) :- /* prints when backing up */ write('*** undoing '), write(Cycle), nl, !, fail. /* The successor function advances the student state. It either uses forward chaining or backwards chaining, depending on the problem. A student state is represented as a pair, ss(Situation,Stack), where the situation is a set of positive ground literals and the stack is a mixture of goals and literals that looks like intend(operator_application). There are several kinds of goals, one for each kind of precondition that operators can have. A call(XXX) precondition/goal results in a call to Prolog, which is handy for doing arithemetic and other calculations. An all(XXX) precondition/goal is useful for putting dynamically created preconditions on the stack (see the first operator of the kine problem for an example). A not(XXX) precondition/goal can only be satified if the literal XXX is not in the current situation. The last kind of precondition/goal is a positive literal that can be satisfied by unifying with a literal in the current state. Unifying such positive-literal preconditions with the current situation is how the variables of operators get bound. Because variables are shared between the preconditions and the operator applications, and all the operators variables appear in the operator application, we save the information generated by unifying the preconditions for eventual use in the operator's effects when it is executed. For the forwards direction, the successor function picks the first operator whose preconditions unify with a literal in the current situation and executes it. For the backwards direction, the basic cycle is to find an operator with an effect that unifies with the top goal on the stack, push that operator application on the stack surrounded in by intend(), push the preconditions of the operator on the stack as subgoals, and recurse. When the problem is installed, a literal of the form direction(XXX) is asserted, and determines which direction the problem solver will take. */ successor(ss(Situation,Stack), do(Op), ss(NextSituation,Stack)) :- direction(forward), /* only do this clause if direction is forward */ !, /* this cut insures that other successor clauses only used for backwards dir */ fetch_operator(Op, Preconds, Effects), all_hold(Preconds, Situation), execute(Situation, Effects, NextSituation). successor(ss(Situation,[intend(Op) | Stack]), do(Op), ss(NewSituation, Stack)) :- fetch_operator(Op,_,Effects), execute(Situation,Effects,NewSituation). successor(ss(Situation,[call(Literal) | Stack]), call(Literal), ss(Situation,Stack)) :- call(Literal). successor(ss(Situation,[all(Goals) | Stack]), all(Goals), ss(Situation,NewStack)) :- strict_disjoint(Goals,Stack), /* prevents infinite recursion */ append(Goals,Stack,NewStack). successor(ss(Situation,[not(Goal) | Stack]), check(not(Goal)), ss(Situation, Stack)) :- not(member(Goal,Situation)). successor(ss(Situation,[Goal | Stack]), check(Goal), ss(Situation, Stack)) :- member(Goal,Situation). successor(ss(Situation, Stack), push(Op), ss(Situation, NewStack)) :- fetch_operator(Op, Preconds, Effects), Stack = [Goal | _], member(Goal,Effects), not(strict_member(do(Op),Stack)), /* prevents infinite recursion */ append(Preconds, [intend(Op) | Stack], NewStack). /* all_hold tests whether a precondition is true in the given situation. Most preconditions are just positive literals, so we use member to see if they unify with a literal in the current situation. A few preconditions are negated literals, so we make sure that they do not unify with anything in the given situation. A few preconditions have the form call(XXX) where XXX is a prolog form. This allows the operator-writer to call prolog forms from inside the precondition lists. Useful for arithmetic and algebraic manipulations. A few preconditions have the form all(XXX) where XXX is a list of in the same form as the preconditions. This is useful for dynamically generated preconditions (see the first operator of the kine problem). */ all_hold([], _). all_hold([not(Condition) | Rest], Situation) :- !, not(member(Condition, Situation)), all_hold(Rest, Situation). all_hold([call(Condition) | Rest], Situation) :- !, call(Condition), all_hold(Rest, Situation). all_hold([all(ListOfConditions) | Rest], Situation) :- !, all_hold(ListOfConditions,Situation), all_hold(Rest,Situation). all_hold([Condition | Rest], Situation) :- member(Condition, Situation), all_hold(Rest, Situation). /* Execute modifies the given situation to make a new one. It adds positive literals and subtracts negative literals. When we subtract negated literals, we must not do unification but equality, otherwise we will get variable bindings that we don't want. Also, to avoid have two copies of exactly the same literal, we use an equality-based member to check for redundancy before adding a positive literal. */ execute(Situation, [], Situation). execute(Situation, [not(Effect) | Rest], Result) :- !, strict_member(Effect, Situation), strict_set_difference(Situation, [Effect], Next), execute(Next, Rest, Result). execute(Situation, [Effect | Rest], Result) :- strict_member(Effect, Situation), !, execute(Situation, Rest, Result). execute(Situation, [Effect | Rest], Result) :- execute([Effect | Situation], Rest, Result). /* ========================== Talking to the student =================== */ /* To make it easier to test the code, don't actually talk to the student. Instead, make the student's entries be part of the problem statement, listed in reverse order. Just pop them off each time get_student_entry is called. */ get_student_entry(Entry) :- entries([Entry | Rest]), retractall(entries(_)), assert(entries(Rest)), write('Student enters '), write(Entry), nl. positive_feedback(Trace) :- write('Good! It looks to me like you did the following mental actions:'), nl, print_list_one_per_line(Trace). negative_feedback :- write('Wrong. '), nl. tutor_is_confused :- write('I cannot find an action to do next.'), nl. tutor_will_follow_student :- write('I cannot understand what you want to do, but do it anyway.'), nl. coach(Trace) :- write('Do these mental actions: '), nl, print_list_one_per_line(Trace). print_list_one_per_line([]). print_list_one_per_line([X|R]) :- write(' '), write(X), nl, print_list_one_per_line(R). /* ====================== utilities for tracing the execution of the tutor ========== */ pstate(ss(Situation,Stack)) :- write('Student Situation is:'), nl, print_list_one_per_line(Situation), write('Student Stack is:'), nl, print_list_one_per_line(Stack), wait_for_ok. pstate(_) :- write('backing up'), nl, !, fail. wait_for_ok :- write('Type a number, a period and the enter key: '), read(_). /* ===================== initialization ====================== */ /* declare that operator can be asserted at runtime */ :- dynamic(operator/3). :- dynamic(enteries/1). :- dynamic(direction/1). /* main initialization. Just installs the problem data and returns the initial student state. See problem section below for documentation of the format of problems */ initialize(Problem,ss(Situation,Stack)) :- install_problem(Problem), operator(start,_,Situation), operator(finish,Stack,_). install_problem(Problem) :- problem(Problem, Direction, Ops, Entries), retractall(direction(_)), assert(direction(Direction)), retractall(operator(_,_,_)), assertall(Ops), retractall(entries(_)), assert(entries(Entries)). /* When the successor function is searching for a chain of operators, we don't want to consider the start and finish operators. */ fetch_operator(Op,Preconds,Effects) :- operator(Op,Preconds,Effects), not(Op=start), not(Op=finish). /* ==================== utilities =================================== */ /* asserts all predicates in the given list, maintaining order */ assertall([]). assertall([P|Rest]) :- assertall(Rest),asserta(P). /* standard predicates that some Prologs do not define. I've commented out the ones that my Prolog has. */ /* not(P) :- call(P), !, fail. not(_). member(X, [X|_]). member(X, [_|L]) :- member(X,L). append([],L,L). append([X|L1],L2,[X|L3]) :- append(L1, L2, L3). */ /* last returns the last element of a list. Can't backtrack into it. */ last([X], X) :- !. last([_|X],Y) :- last(X,Y). /* tests whether first arg is a member of second arg using strict equality */ strict_member(X1, [X2|_]) :- X1 == X2, !. strict_member(X, [_|L]) :- strict_member(X,L). set_difference([], _, []). set_difference([X | Rest], Subtrahend, Result) :- not(not(member(X, Subtrahend))), !, set_difference(Rest, Subtrahend, Result). set_difference([X | Rest], Subtrahend, [X | Result]) :- set_difference(Rest, Subtrahend, Result). strict_set_difference([], _, []). strict_set_difference([X | Rest], Subtrahend, Result) :- strict_member(X, Subtrahend), !, strict_set_difference(Rest, Subtrahend, Result). strict_set_difference([X | Rest], Subtrahend, [X | Result]) :- strict_set_difference(Rest, Subtrahend, Result). /* true if the sets are disjoint (no common member) */ strict_disjoint([],_). strict_disjoint([X|_],Set) :- strict_member(X,Set), !, fail. strict_disjoint([_|R],Set) :- strict_disjoint(R,Set). /* ======================== test problems ============================= */ /* A problem consists of a name (so it can be conveniently called from the top level), the solver direction (forward or backwards), a set of Strips operators, and the student's actions. The student's actions must end with i_quit. A Strips operator consists of its name (a predicate that includes all the variables needed to instantiate the operator), the preconditions (a set of literals and other forms) and the effects (a set of literals). The start operator specifies the initial state in its effects; the finish operator specifies the top level goals in its preconditions. In order to simplify the determination of when a plan is solved, the initial state must have only ground literals (no variables) and the variables in an operator's effects must appear in its preconditions (see page Russell & Norvig, 359). */ /* Operators in the following problem have no arguments so this should be good for debugging. Backward chaining wants to do the right sock and shoe then the left sock and shoe. First we try doing the procedure completely correctly. */ problem(shoes1, /* problem name */ backwards, /* use backwards chaining on this problem */ [ /* operators */ operator(left_shoe, [left_sock_on, not(left_shoe_on)], [left_shoe_on]), operator(right_sock, [not(right_sock_on)], [right_sock_on]), operator(left_sock, [not(left_sock_on)], [left_sock_on]), operator(right_shoe, [right_sock_on, not(right_shoe_on)], [right_shoe_on]), operator(start,[],[]), operator(finish,[right_shoe_on,left_shoe_on],[])], [ /* student actions */ right_sock, right_shoe, left_sock, left_shoe, i_quit]). /* See what happens with maximal help. */ problem(shoes2, /* problem name */ backwards, /* use backwards chaining on this problem */ [ /* operators */ operator(left_shoe, [left_sock_on, not(left_shoe_on)], [left_shoe_on]), operator(right_sock, [not(right_sock_on)], [right_sock_on]), operator(left_sock, [not(left_sock_on)], [left_sock_on]), operator(right_shoe, [right_sock_on, not(right_shoe_on)], [right_shoe_on]), operator(start,[],[]), operator(finish,[right_shoe_on,left_shoe_on],[])], [ /* student actions */ i_am_stuck, i_am_stuck, i_am_stuck, i_am_stuck, i_quit]). /* If we put both socks on first, backwards chaining can't parse the student's actions. */ problem(shoes3, /* problem name */ backwards, /* use backwards chaining on this problem */ [ /* operators */ operator(left_shoe, [left_sock_on, not(left_shoe_on)], [left_shoe_on]), operator(right_sock, [not(right_sock_on)], [right_sock_on]), operator(left_sock, [not(left_sock_on)], [left_sock_on]), operator(right_shoe, [right_sock_on, not(right_shoe_on)], [right_shoe_on]), operator(start,[],[]), operator(finish,[right_shoe_on,left_shoe_on],[])], [ /* student actions */ right_sock, left_sock, right_shoe, left_shoe, i_quit]). /* If we put both socks on first but allow forward chaining, then all is okay. */ problem(shoes4, /* problem name */ forward, /* use forward chaining on this problem */ [ /* operators */ operator(left_shoe, [left_sock_on, not(left_shoe_on)], [left_shoe_on]), operator(right_sock, [not(right_sock_on)], [right_sock_on]), operator(left_sock, [not(left_sock_on)], [left_sock_on]), operator(right_shoe, [right_sock_on, not(right_shoe_on)], [right_shoe_on]), operator(start,[],[]), operator(finish,[right_shoe_on,left_shoe_on],[])], [ /* student actions */ right_sock, left_sock, right_shoe, left_shoe, i_quit]). /* This is a classic planning problem taken from Russell & Norvig's AI textbook. sm = supermarket and hws = hardware store. In shop1, the student does a correct sequence of actions. The preconditions of buy(X,Store) are ordered to make the search efficient. */ problem(shop1, backwards, [ operator(buy(X,Store), [sells(Store,X), at(Store), not(have(X))], [have(X)]), operator(go(Here,There), [at(Here), place(There), call(not(Here=There))], [at(There), not(at(Here))]), operator(start, [], [at(home), sells(hws,drill), sells(sm,milk), sells(sm,bananas), place(hws), place(sm), place(home)]), operator(finish, [have(drill), have(milk), have(bananas), at(home)], [])], [ go(home,hws),buy(drill,hws),go(hws,sm), buy(milk,sm),buy(bananas,sm),go(sm,home),i_quit]). /* The student forgets to buy the bananas. The tutor makes the bizzare assumption that the student went home first and planned to returned to the store to buy the bananas. The operator definitions allow this, because they don't try to take the shortest route (zero in this case) in order to get somewhere. */ problem(shop2, backwards, [ operator(buy(X,Store), [sells(Store,X), at(Store), not(have(X))], [have(X)]), operator(go(Here,There), [at(Here), place(There), call(not(Here=There))], [at(There), not(at(Here))]), operator(start, [], [at(home), sells(hws,drill), sells(sm,milk), sells(sm,bananas), place(hws), place(sm), place(home)]), operator(finish, [have(drill), have(milk), have(bananas), at(home)], [])], [ go(home,hws),buy(drill,hws),go(hws,sm), buy(milk,sm),go(sm,home),i_quit]). /* a famous blocks world problem: the Sussman anomaly. Backwards chaining cannot solve this problem. Let's have the student do it right and see if the tutor can follow along. This illustrates what happens when the student is smarter than the expert model. */ problem(sussman1, backwards, [ operator(move(B,X,Y), [on(B,X),clear(B),clear(Y), call(not(X=B)), call(not(Y=B)), call(not(X=Y))], [on(B,Y),clear(X),not(on(B,X)),not(clear(Y))]), operator(start, [], [on(b,t1),on(a,t2),on(c,a),clear(t3),clear(b),clear(c)]), operator(finish, [on(a,b),on(b,c)], [])], [ move(c,a,t3), move(b,t1,c), move(a,t2,b), i_quit]). /* well, backwards didn't work. Try it in the forwards direction. */ problem(sussman2, forward, [ operator(move(B,X,Y), [on(B,X),clear(B),clear(Y), call(not(X=B)), call(not(Y=B)), call(not(X=Y))], [on(B,Y),clear(X),not(on(B,X)),not(clear(Y))]), operator(start, [], [on(b,t1),on(a,t2),on(c,a),clear(t3),clear(b),clear(c)]), operator(finish, [on(a,b),on(b,c)], [])], [ move(c,a,t3), move(b,t1,c), move(a,t2,b), i_quit]). /* a simple kinematics problem. The first operator is a meta-interpreter. It looks for an equation then backwards chains to get its quantities known. The other operators just produce the equations. If you reverse the order of the two equation operators, then the tutor will fail. Running demo(kine1) reveals why. The expert first writes an equation that doesn't contain velocity(2), the sought quantity. It then backs up and writes the right equation. The tutor can't tutor backing up over physical actions. */ problem(kine1, backwards, [operator(solve_E_for_Q(Eqn,Q), /* if the equation contains Q, and you can subgoal to make the other quantities in the equation known, then Q is known. */ [eqn(Eqn), call(eqn_contains_Q(Eqn,Q)), not(known(Q)), call(quantities_but_Q(Eqn,Q,Quantities)), all(Quantities)], [known(Q)]), operator(avg_v_is_average_of_vi_and_vf(T1,T2), [time_pt(T1), time_pt(T2), call(T1< T2), not(eqn(avg_v(T1,T2) = (velocity(T1) + velocity(T2)) / 2))], [eqn(avg_v(T1,T2) = (velocity(T1) + velocity(T2)) / 2)]), operator(displacement_is_avg_v_times_duration(T1,T2), [time_pt(T1), time_pt(T2), call(T1< T2), not(eqn(displacement(T1,T2) = avg_v(T1,T2) * duration(T1,T2)))], [eqn(displacement(T1,T2) = avg_v(T1,T2) * duration(T1,T2))]), operator(start, [], [time_pt(1), time_pt(2), known(displacement(1,2)), known(duration(1,2)), known(velocity(1))]), operator(finish, [known(velocity(2))], [])], [ i_am_stuck, displacement_is_avg_v_times_duration(1,2), solve_E_for_Q(displacement(1,2) = avg_v(1,2) * duration(1,2), avg_v(1,2)), solve_E_for_Q(avg_v(1,2) = (velocity(1) + velocity(2)) / 2, velocity(2)), i_quit] ). /* This version solves the kinematics problem with forward search. Note that the equations must be done in a different order. */ problem(kine2, forward, [operator(solve_E_for_Q(Eqn,Q), /* if the equation contains Q, and you can subgoal to make the other quantities in the equation known, then Q is known. */ [eqn(Eqn), call(eqn_contains_Q(Eqn,Q)), not(known(Q)), call(quantities_but_Q(Eqn,Q,Quantities)), all(Quantities)], [known(Q)]), operator(avg_v_is_average_of_vi_and_vf(T1,T2), [time_pt(T1), time_pt(T2), call(T1< T2), not(eqn(avg_v(T1,T2) = (velocity(T1) + velocity(T2)) / 2))], [eqn(avg_v(T1,T2) = (velocity(T1) + velocity(T2)) / 2)]), operator(displacement_is_avg_v_times_duration(T1,T2), [time_pt(T1), time_pt(T2), call(T1< T2), not(eqn(displacement(T1,T2) = avg_v(T1,T2) * duration(T1,T2)))], [eqn(displacement(T1,T2) = avg_v(T1,T2) * duration(T1,T2))]), operator(start, [], [time_pt(1), time_pt(2), known(displacement(1,2)), known(duration(1,2)), known(velocity(1))]), operator(finish, [known(velocity(2))], [])], [displacement_is_avg_v_times_duration(1,2), solve_E_for_Q(displacement(1,2) = avg_v(1,2) * duration(1,2),avg_v(1,2)), avg_v_is_average_of_vi_and_vf(1,2), solve_E_for_Q(avg_v(1,2) = (velocity(1) + velocity(2)) / 2, velocity(2)), i_quit] ). /* helper code for the kinematics problem */ /* eqn_contains is true if the given equation contains the given quanitity. Treats the expression as a tree and does a tree search. A branch in the tree occurs only when there is an arithmetic operator. Everything else is a leaf. If the leaf unifies with the quantity, then we succeed. If the leaf does not unify, then fail, which causes the tree search to move to another leaf. */ eqn_contains_Q(-(Expr),Q) :- !, eqn_contains_Q(Expr, Q). eqn_contains_Q(Expr, Q) :- functor(Expr, Name, 2), /* true if Expr is a binary function with name Name */ member(Name, [*,-,+,=,/]), !, arg(1, Expr, LHS), arg(2, Expr, RHS), (eqn_contains_Q(LHS,Q) ; eqn_contains_Q(RHS,Q)). /* semi-colon means OR */ eqn_contains_Q(Q,Q). eqn_contains_Q_2(LHS,_, Q) :- eqn_contains_Q(LHS,Q). eqn_contains_Q_2(_,RHS, Q) :- eqn_contains_Q(RHS,Q). /* returns all quanitites in the given equation except for Q. The returned quantities are wrapped in known(XXX) so that they can be used directly as preconditions. */ quantities_but_Q(Eqn,Q,Result) :- quantities_but_Q(Eqn,Q,[],Result). /* initialize the Results variable and call the 4-argument version of quantities_but_Q */ quantities_but_Q(-(Expr), Q, Old, New) :- quantities_but_Q(Expr,Q,Old,New), !. quantities_but_Q(Expr, Q, R1, R3) :- functor(Expr, Name, 2), member(Name, [*,-,+,=,/]), !, arg(1, Expr, LHS), arg(2, Expr, RHS), quantities_but_Q(LHS, Q, R1, R2), quantities_but_Q(RHS, Q, R2, R3). quantities_but_Q(N, _, Result, Result) :- integer(N), /* currently, we only recognize integer constants */ !. quantities_but_Q(Q1, Q2, Result, Result) :- Q1 == Q2, /* don't put the target quantity in the output; use strict equality */ !. quantities_but_Q(X, _, Result, Result) :- strict_member(known(X),Result), !. quantities_but_Q(X, _, Result, [known(X) | Result]) :- !. /* include a quantity only if it is not a strict member of the results already */