☰

A "Hands-on" Introduction to
Pure Logic Programming
(Pure Prolog)

Computational Logic


Syntax: Terms (Variables, Constants, and Structures)

We will be using the Prolog language (just the pure part in this set of slides), so we use Prolog notation conventions:

  • Variables: start with uppercase character (or "_"), may include "_" and digits:

    Examples: X, Im4u, A_little_garden, _, _x, _22

  • Constants: lowercase first character, may include "_" and digits. Also, numbers and some special characters. Quoted, any character:

    Examples: a, dog, a_big_cat, 23, 'Hungry man', []

  • Structures: a functor (the structure name, is like a constant name) followed by a fixed number of arguments between parentheses:

    Example: date(monday, Month, 1994)

    Arguments can in turn be variables, constants and structures.

    • Arity: is the number of arguments of a structure. Functors are represented as name/arity. A constant can be seen as a structure with arity zero.
Variables, constants, and structures as a whole are called terms (they are the terms of a "first–order language"): the data structures of a logic program.

Syntax: Terms

(using Prolog notation conventions)

  • Examples of terms:

    TermTypeMain functor:
    dadconstant dad/0
    time(min, sec)structure time/2
    pair(Calvin, tiger(Hobbes))structure pair/2
    Tee(Alf, rob)illegal —
    A_good_timevariable —

  • A variable is free if it has not been assigned a value yet.

  • A term is ground if it contains no free variables.

  • Functors can be defined as prefix, postfix, or infix operators (just syntax!):

    a + bis the term +(a,b)if +/2 declared infix
    - bis the term -(b)if -/1 declared prefix
    a < bis the term <(a,b)if </2 declared infix
    john father maryis the term father(john,mary)if father/2 declared infix

    We assume that some such operator definitions are always preloaded.

Syntax: Rules and Facts (Clauses)

  • Fact: an expression of the form where the are terms.

  • Rule: an expression of the form:

    • to are syntactically like terms.
    • is called the head of the rule.
    • The to the right of the arrow are called literals and form the body of the rule.
      They are also called procedure calls.
    • :- is called the neck of the rule.
    Example:

    meal(soup, beef, coffee).               % <- A fact.
    meal(First, Second, Third) :-           % <- A rule.
         appetizer(First),                  %
         main_dish(Second),                 %
         dessert(Third).                    %
  • A fact is a rule with an empty (true) body (i.e., equiv to ).

  • Rules and facts are both called clauses.

Syntax: Predicates, Programs, and Queries

  • Predicate (or procedure definition): a set of clauses whose heads have the same name and arity (called the predicate name).

    Examples:

    pet(spot).                            animal(tim).
    pet(X) :- animal(X), barks(X).        animal(spot). 
    pet(X) :- animal(X), meows(X).        animal(hobbes). 
    Predicate pet/1 has three clauses. Of those, one is a fact and two are rules. Predicate animal/1 has three clauses, all facts.

  • Logic Program: a set of predicates.

  • Query: an expression of the form:

    (i.e., a clause without a head).
    A query represents a question to the program.
    Example: ?- pet(X).

"Declarative" Meaning of Facts and Rules

The declarative meaning is the corresponding one in first order logic, according to certain conventions:
  • Facts: state things that are true.
    (Note that a fact "p." can be seen as the rule " p :- true. ")

    Example: the fact animal(spot).
    can be read as "spot is an animal".

  • Rules:
    • Commas in rule bodies represent conjunction, and
      ":-" represents logical implication (backwards, i.e., if).
    • i.e., . represents .
    Thus, a rule . means "if and … and are true, then is true"

    Example: the rule pet(X) :- animal(X), barks(X).
    can be read as "X is a pet if it is an animal and it barks".

  • Variables in facts and rules are universally quantified, ∀ (recall clausal form!).

"Declarative" Meaning of Predicates and Queries

  • Predicates: clauses in the same predicate

    p :- p_1, ..., p_n
    p :- q_1, ..., q_m
    ...
    provide different alternatives (for p).
    Example: the rules

    pet(X) :- animal(X), barks(X). 
    pet(X) :- animal(X), meows(X). 
    express two alternative ways for X to be a pet.
  • Note (variable scope): the X variables in the two clauses above are different, despite the same name. Vars. are local to clauses (and are renamed any time a clause is used –as with vars. local to a procedure in conventional languages).

  • A query represents a question to the program.
    Examples:

    ?- pet(spot).?- pet(X).
    Asks: Is spot a pet? Asks: "Is there an X which is a pet?"

"Execution" and Semantics

  • Example of a logic program:

    A first program: the pets database.

    :- module(_,_,[]).
    
    pet(X) :-
        animal(X),
        barks(X).
    pet(X) :-
        animal(X),
        meows(X).
    
    animal(tim).
    animal(spot).
    animal(hobbes).
    
    barks(spot).
    
    meows(tim).
    
    roars(hobbes).
    ?- pet(spot).
    ?- pet(X).
  • Execution: given a program and a query, executing the logic program is attempting to find an answer to the query.

    Example: given the program above and the query ?- pet(X).
    the system will try to find a "substitution" for X which makes pet(X) true.

    • The declarative semantics specifies what should be computed
      (all possible answers).
      ⇒ Intuitively, we have two possible answers: X = spot and X = tim.

    • The operational semantics specifies how answers are computed
      (which allows us to determine how many steps it will take).

Running Programs in a Prolog System

  • Interaction with the system query evaluator (the "top level"):

    Ciao X.Y-...
    ?- use_module(pets).
    yes
    ?- pet(spot).
    yes
    ?- pet(X).
    X = spot ? ;
    X = tim ? ;
    no
    ?- 
See the part on "Developing Programs with a Prolog System"
for more details on the particular system used in the course (Ciao).

Simple (Top-Down) Operational Meaning of Programs

  • A logic program is operationally a set of procedure definitions (the predicates).

  • A query ?- p. is an initial procedure call.

  • A procedure definition with one clause p :- p,…,p.
    means: "to execute a call to p you have to call p and … and p"

    • In principle, the order in which p, …, p are called does not matter, but in practical systems it is fixed.

  • If the procedure has several clauses (i.e., several definitions):

    p :- p_1, ..., p_n
    p :- q_1, ..., q_m
    ...
    it means: "to execute a call to p, call p and … and p, or, alternatively, q and … and q, or …"

    • Unique to logic programming –it is like having several alternative procedure definitions.
    • Means that several possible paths may exist to a solution and they should be explored.
    • System usually stops when the first solution found, user can ask for more.
    • Again, in principle, the order in which these paths are explored does not matter (if certain conditions are met), but, for a given system, this is typically also fixed.
In the following we define a more precise operational semantics.

Unification: A fundamental Operation with Multiple Uses

  • Unification (=/2) is an operation which finds the conditions needed for two terms to be equal:
    • E.g.:    f(a,g(X)) = f(Y,g(b))

      if Y=a  and  X=b.
    It has many uses:
    • It is the mechanism used in procedure calls to:
      • Pass parameters.
      • "Return" values.
    • It is also used to:
      • Access parts of structures.
      • Give values to variables.
  • It is a procedure to solve equations on data structures.
    • As usual, it returns a minimal solution to the equation (or the equation system).
    • As many equation solving procedures it is based on isolating variables and then instantiating them with their values.

Unification (more formally)

  • Unifying two terms (or literals) and : is asking if they can be made syntactically identical by giving (minimal) values to their variables.
    • I.e., find a variable substitution such that (or, if not possible, fail).
    • Only variables can be given values!
    • Two structures can be made identical only by making their arguments identical.
    E.g.:

    ≡
    f(X,g(t))f(m(h),g(M))X=m(h), M=tf(m(h),g(t))f(m(h),g(t))
    dogdog∅dogdog
    dogcatfail
    XaX=aaa
    XYX=YYY
    f(X,g(t))f(m(h),t(M))fail (1)
    Xf(X)fail (2)

  • (1) Structures with different name and/or arity cannot be unified.
  • (2) A variable cannot be given as value a term which contains that variable, because it would create an infinite term. This is known as the occurs check. (See, however, cyclic terms later.)

Unification

  • Several solutions can exist, e.g.:

    and
    f(X)f(m(H))X=m(a), H=af(m(a))
    ""X=m(g(b)), H=g(b)f(m(g(b)))
    ...

    These are correct, but we want the solution that is minimal (that binds minimally the variables). In this case it is:

    and
    f(X)f(m(H))X=m(H)f(m(H))

    Note that the result, f(m(H)), is "more general" than f(m(a)) or f(m(g(b))).

  • This minimal or most general solution always exists (unless unification fails), and is unique, modulo variable renaming.
  • The unification algorithm finds this solution.

Unification Algorithm

Given a set of one or more equations: , , …
  • Initialize the solution to empty.
  • Until no more equations left, do:

    • select an equation ,
    • delete it, and,

depending on the form of equation : do:
X=Xignore
X=f(..., X, ...)fail (occurs check)
X=add X= to the solution and replace X by anywhere else
a=aignore
a=bfail
a=f(...)fail
g(...)=f(...)fail
f(......)=f(......) () fail
f(,...,)=f(,...,) add to the system: =, …, =

Unification Algorithm Examples

Some examples of unifications: do them first by hand! (following the unification algorithm) and check them after in the Prolog top level

Try:

?- p(X,f(b)) = p(a,Y).
?- p(X,f(Y)) = p(a,g(b)).
?- p(X,X) = p(f(Z),f(W)).
?- p(X,f(Y)) = p(Z,X).
?- p(X,f(X)) = p(Z,Z).
(Note that X=f(X) is an infinite term - really a constraint)

With this last case we can try also unify_with_occurs_check/1 (from the iso_misc library):

?- use_module(library(iso_misc)), unify_with_occurs_check( p(X,f(X)), p(Z,Z) ).
Can you explain the difference between the two answers?

  • Unify: p(X,f(b)) and p(a,Y)

    p(X,f(b))p(a,Y)Xa
    Yf(b)

  • Unify: p(X,f(Y)) and p(a,g(b))

    p(X,f(Y))p(a,g(b))Xa
    f(Y)g(b)fail

  • Unify: p(X,X) and p(f(Z),f(W))

    p(X,X)p(f(Z),f(W))Xf(Z)Xf(Z)Xf(Z)Xf(W)
    Xf(W)f(Z)f(W)ZWZW

  • Unify: p(X,f(Y)) and p(Z,X)

    p(X,f(Y))p(Z,X)XZX=ZXf(Y)
    f(Y)Xf(Y)ZZf(Y)

  • Unify: p(X,f(X)) and p(Z,Z)

    p(X,f(X))p(Z,Z)XZXZ
    f(X)Zf(Z)Zfail   ("Occurs check")

A Schematic Interpreter for Logic Programs (SLD–resolution)

Input: A logic program , a query
Output: (answer substitution) if is provable from , failure otherwise

  1. Make a copy of
  2. Initialize the "resolvent" to be
  3. While is nonempty do:
    1. Take a literal in
    2. Take a clause (renamed) from
      with same predicate symbol as
      1. If there is a solution to (unification)
        • Replace in by
        • Apply to and
      2. Otherwise, take another clause and repeat
    3. If there are no more clauses, go back to some other choice
    4. If there are no pending choices left, output failure
  4. ( empty) Output solution to
  5. Explore another pending branch for more solutions (upon request)

The Choices Made in Standard Prolog

Input: A logic program , a query
Output: (answer substitution) if is provable from , failure otherwise

  1. Make a copy of
  2. Initialize the "resolvent" to be
  3. While is nonempty do:
    1. Take the leftmost literal in
    2. Take the first clause (renamed) from
      with same predicate symbol as
      1. If there is a solution to (unification)
        • Replace in by
        • Apply to and
      2. Otherwise, take the next clause and repeat
    3. If there are no more clauses, go back to most recent pending choice
    4. If there are no pending choices left, output failure
  4. ( empty) Output solution to
  5. Explore the most recent pending branch for more solutions (upon request)

A Schematic Interpreter for Logic Programs (Contd.)

  • Step 3.2 defines alternative paths to be explored to find answer(s);
    execution explores this tree (for example, breadth-first).
  • Since step 3.2 is left open, a given logic programming system must specify how it deals with this by providing one (or more):
    • Search rule(s): "how are clauses/branches selected in 3.2."
  • Note that choosing a different clause (in step 3.2) can lead to finding solutions in a different order – Example (two valid executions):

    ?- pet(X).                          ?- pet(X).     
    X = spot ? ;                        X = tim ? ;  
    X = tim ? ;                         X = spot ? ;   
    no                                  no             
    ?-                                  ?-             
  • In fact, there is also some freedom in step 3.1, i.e., a system may also specify:

    • Computation rule(s): "how are literals selected in 3.1."

Running Programs: Alternative Execution Paths

C: pet(X) :- animal(X), barks(X).
C: pet(X) :- animal(X), meows(X).
C: animal(tim).    C: barks(spot).
C: animal(spot).   C: meows(tim).
C: animal(hobbes). C: roars(hobbes).

  • ?- pet(X). (top-down, left-to-right)
Clause
pet(X)pet(X)C* X=X
pet(X)animal(X), barks(X)C* X=tim
pet(tim)barks(tim)???failure
  • * means choice-point, i.e., other clauses applicable.

  • But solutions exist in other paths!

  • → Let's go back to our last choice point (C*) and try the next alternative...

Running Programs: Alternative Execution Paths

C: pet(X) :- animal(X), barks(X).
C: pet(X) :- animal(X), meows(X).
C: animal(tim).    C: barks(spot).
C: animal(spot).   C: meows(tim).
C: animal(hobbes). C: roars(hobbes).

  • ?- pet(X). (top-down, left-to-right, different branch)
Clause
pet(X)pet(X)C* X=X
pet(X)animal(X), barks(X)C* X=spot
pet(spot)barks(spot)C
  • * means choice-point, i.e., other clauses applicable.

  • System response: X = spot   ?   

  • If we type ; after the ? prompt (i.e., we ask for another solution) the system can go and execute a different branch (i.e., a different choice in C, or C).

Running Programs: The "normal programming" View

  • The resolution interpreter is the formal basis behind the execution.
  • However, in practice one can follow standard Prolog (i.e., depth-first, left-to-right) execution, as it goes step by step through code, similarly to any other language.
  • This is easy to see with the debugger:

?- pet(X).
   1  1  Call: draft:pet(_15363) ? 
   2  2  Call: draft:animal(_15363) ? 
   2  2  Exit: draft:animal(tim) ? 
   3  2  Call: draft:barks(tim) ? 
   3  2  Fail: draft:barks(tim) ? 
   2  2  Redo: draft:animal(tim) ? 
   2  2  Exit: draft:animal(spot) ? 
   3  2  Call: draft:barks(spot) ? 
   3  2  Exit: draft:barks(spot) ? 
   1  1  Exit: draft:pet(spot) ? 
X = spot ? ;
   1  1  Redo: draft:pet(spot) ? 
   2  2  Redo: draft:animal(spot) ? 
   2  2  Exit: draft:animal(hobbes) ? 
   3  2  Call: draft:barks(hobbes) ? 
   3  2  Fail: draft:barks(hobbes) ? 
   2  2  Call: draft:animal(_15363) ? 
   2  2  Exit: draft:animal(tim) ? 
   3  2  Call: draft:meows(tim) ? 
   3  2  Exit: draft:meows(tim) ? 
   1  1  Exit: draft:pet(tim) ?
X = tim ? 
  • Procedure Call: and Exit: (return) is as standard.
  • The major difference is that, sometimes, if a step fails (Fail:), then execution can backtrack (Redo:) to the last choice point.
    • This allows search and having multiple answers.
  • In the IDEs each execution step is marked on the source.

Comparison with Imperative and Functional Languages

  • Conventional languages and Prolog both implement (forward) continuations:
    the place to go after a procedure call succeeds. I.e., in:

    p(X,Y):- q(X,Z), r(Z,Y).
    q(X,Z) :- ...

    when the procedure call to q/2 finishes (Exit:), execution continues in p/2, just after the call to q/2, i.e., at the call to r/2   –the forward continuation.

  • In Prolog, when there are procedures with multiple definitions, there is also a backward continuation: the place to go to if there is a failure (Redo:). I.e., in:

    p(X,Y):- q(X,Z), r(Z,Y).
    p(X,Y):- ...

    q(X,Z) :- ...

    if q/2 succeeds, it is as above, but if it fails, execution continues at (backtracks to) the previous alternative: the second clause of p/2   –the backward continuation.

  • We say that p/2 has a choice point.
  • Again, the debugger is very useful to observe how execution proceeds.

Comparison with Imperative and Functional Languages (Contd.)

  • Programs without search (that do not perform "deep" backtracking):

    • Generally (if no disjunction etc. used) this means programs that:
      • Have only one clause per procedure, or
      • if several clauses, only one of them selected for every call to that predicate.
      Note that this is dependent on call mode, i.e., which variables are bound on a given call.

    • → Because of the left-to-right rule, these "normal" programs run in Prolog quite similarly to their imperative and (strict) functional counterparts.
    • → Imperative/ functional programs can be directly expressed and run!
  • Programs with search (perform "deep" backtracking):
    • These are programs that have at least one procedure that:
      • has multiple clauses, and
      • more than one of them is selected for some calls to that procedure.
      Again, this is dependent on call mode.

    • → These programs perform search (backtracking-based, or other search rules).
    • → They have no direct counterparts in imperative or functional programming.

Role of Unification in Execution

  • As mentioned before, unification used to access data and give values to variables.

    Example: Consider query
    ?- animal(A), named(A,Name).
    with:

    animal(dog(tim)).
    named(dog(Name),Name).
    The call to animal(A) succeeds with A=dog(tim).
    In order to access the name we call named(A,Name) which binds Name=tim.
    We could have also done simply: ?- animal(A), A=dog(Name).

  • Also, unification is used to pass parameters in procedure calls and to return values upon procedure exit. Here a value spot is returned in P:

    Clause
    pet(P)pet(P)C* P=X
    pet(X)animal(X), barks(X)C* X=spot
    pet(spot)barks(spot)C
    pet(spot)———

    Answer: P=spot

"Modes"

  • In fact, argument positions are not fixed a priory to be input or output.

    Example: Consider query ?- pet(spot). vs. ?- pet(X).

    :- module(_,_,[]).
    
    pet(X) :-
        animal(X),
        barks(X).
    pet(X) :-
        animal(X),
        meows(X).
    
    animal(tim).
    animal(spot).
    animal(hobbes).
    
    barks(spot).
    
    meows(tim).
    
    roars(hobbes).
    ?- pet(spot).
    ?- pet(X).
    or in the Peano arithmetic example from the introduction:

         ?- plus( s(0),  s(s(0)), Z).           % Adds 
    vs.  ?- plus( s(0),  Y,       s(s(s(0)))).  % Subtracts
    :- module(_,_,[sr/bfall]).
    
    nat(0).
    nat(s(X)) :- nat(X).
    
    le(0,X) :- nat(X).
    le(s(X),s(Y)) :- le(X,Y).
    
    add(0,Y,Y) :- nat(Y).
    add(s(X),Y,s(Z)) :- add(X,Y,Z).
    
    mult(0,Y,0) :- nat(Y).
    mult(s(X),Y,Z) :- add(W,Y,Z), mult(X,Y,W).
    
    nat_square(X,Y) :- nat(X), nat(Y), mult(X,X,Y).
    
    output(X) :- nat(Y), le(Y,s(s(s(s(s(0)))))), nat_square(Y,X).
    ?- add( s(0), s(s(0)), Z).
    ?- add( s(0), Y,       s(s(s(0)))).
  • Thus, procedures can be used in different modes
    s.t. different sets of arguments are input or output in each mode.
  • We sometimes use + and - to refer to, respectively, and argument being an input or an output, e.g.:
    plus(+X, +Y, -Z)    means we call plus with
    • X instantiated, and
    • Y instantiated,
    and we expect Z to be bound if plus/3 succeeds.

Pure Logic Programming Examples
(Non-Recursive)

Computational Logic

Pure Logic Programs (Overview)

  • Programs that only make use of unification
    (i.e., what we have described so far).
  • They are fully "logical:"
    the set of computed answers "coincides" with the set of logical consequences.
    • Computed answers: the answers for all queries that terminate successfully.
  • Allow programming declaratively, i.e.:
    describe the problem, make queries, obtain correct answers
    → specifications as programs
  • They have full computational power (Turing completeness).
(Recall the initial slides for the course.)

Database Programming

  • A Logic Database is a set of facts and rules (i.e., a logic program):

    :- module(_,_,[sr/bfall]).
    
    father_of(john, peter).
    father_of(john, mary).
    father_of(peter, michael). 
    
    mother_of(mary, david).
    mother_of(jill, john).
    
    grandfather_of(L,M) :-
        father_of(L,N),
        father_of(N,M).
    grandfather_of(X,Y) :-
        father_of(X,Z),
        mother_of(Z,Y).  
  • Given such a logic database, a logic programming system can answer questions (queries) such as:

    ?- father_of(john,peter).
    ?- father_of(john,david).
    ?- father_of(john,X).
    ?- grandfather_of(X,michael).
    ?- grandfather_of(X,Y).
    ?- grandfather_of(X,X).

Database Programming (Contd.)

A circuit topology example:

  • Representing the circuit:

    :- module(_,_,[sr/bfall]).
    
    resistor(power,n1).
    resistor(power,n2). 
    
    transistor(n2,ground,n1).
    transistor(n3,n4,n2).
    transistor(n5,ground,n4).
  • Some circuit 'theory':

    inverter(Input,Output) :- 
       transistor(Input,ground,Output), 
       resistor(power,Output).
    
    nand_gate(Input1,Input2,Output) :-
       transistor(Input1,X,Output),
       transistor(Input2,ground,X), 
       resistor(power,Output).
    
    and_gate(Input1,Input2,Output) :-
       nand_gate(Input1,Input2,X), 
       inverter(X, Output).
  • Some queries to find gates in the circuit:

    ?- inverter(In,Out).
    ?- and_gate(In1,In2,Out).
    ?- nand_gate(In1,In2,Out).

Structured Data and Data Abstraction (and the '=' Predicate)

  • Data structures are created using (complex) terms.
  • Structuring data is important. Consider:

    course(complog, wed, 18, 30, 20, 30, 'M.', 'Hermenegildo', new, 5102).
    course(ai,      thu, 15, 00, 17, 00, 'J.', 'Smith',        old, 3102).
    course(os,      wed, 18, 30, 20, 30, 'L.', 'Hubbard',      new, 6201).
    ...
  • When is the Computational Logic course?
    ?- course(complog, Day, StartH, StartM, FinishH, FinishM, A, B, C, D).
  • Structured version:

    course(complog, Time, Lecturer, Location) :-
        Time = t(wed,18:30,20:30), 
        Lecturer = lect('M.','Hermenegildo'),
        Location = loc(new,5102).

    Note:   X=Y   is equivalent to   =(X,Y)
    where predicate =/2 is defined as the fact =(X,X). – i.e., unification.

  • The course clause can also be written simply as:
course(complog,t(wed,18:30,20:30),lect('M.','Hermenegildo'),loc(new,5102)).

Structured Data and Data Abstraction (and The Anonymous Variable)

  • Given:

    course(complog,Time,Lecturer, Location) :-
          Time = t(wed,18:30,20:30), 
          Lecturer = lect('M.','Hermenegildo'),
          Location = loc(new,5102).
  • When is the Computational Logic course?
    ?- course(complog, Time, A, B).
    has solution:
    Time=t(wed,18:30,20:30), A=lect('M.','Hermenegildo'), B=loc(new,5102)
  • Using the anonymous variable ("_"):
    ?- course(complog,Time, _, _).
    has solution:
    Time=t(wed,18:30,20:30)

Terms as Data Structures with Pointers

  • main below is a procedure, that:
    • creates some data structures, with pointers and aliasing.
    • calls other procedures, passing to them pointers to these structures.
main :- 
    X=f(K,g(K)),
    Y=a, 
    Z=g(L),
    W=h(b,L), 
% Heap memory at this point ---> 
    p(X,Y), 
    q(Y,Z), 
    r(W).

  • Terms are data structures with pointers.
  • Logical variables are declarative pointers.
    • Declarative: they can only be assigned once.

Structured Data and Data Abstraction (Contd.)

The circuit topology example revisited

  • Using data structures to name the components:

    :- module(_,_,[sr/bfall]).
    
    resistor(r1,power,n1).
    resistor(r2,power,n2). 
    
    transistor(t1,n2,ground,n1).
    transistor(t2,n3,n4,n2).
    transistor(t3,n5,ground,n4).
    
    inverter(inv(T,R),Input,Output) :-
        transistor(T,Input,ground,Output),
        resistor(R,power,Output). 
    
    nand_gate(nand(T1,T2,R),Input1,Input2,Output) :-
        transistor(T1,Input1,X,Output),
        transistor(T2,Input2,ground,X),
        resistor(R,power,Output). 
    
    and_gate(and(N,I),Input1,Input2,Output) :-
        nand_gate(N,Input1,Input2,X),
        inverter(I,X,Output).
  • Some queries:

    ?- inverter(Inv,I,O).
    ?- and_gate(G,In1,In2,Out).
    ?- resistor(R1,P1,P2).
    ?- transistor(T1,P1,P2,P3).
  • The query ?- and_gate(G,In1,In2,Out).
    has solution: G=and(nand(t2,t3,r2),inv(t1,r1)),In1=n3,In2=n5,Out=n1

Logic Programs and the Relational DB Model

Relational DatabaseLogic Programming
Relation Name →Predicate symbol
Relation →Procedure consisting of ground facts
(facts without variables)
Tuple →Ground fact
Attribute →Argument of predicate

"Person"

NameAgeSex
Brown20M
Jones21F
Smith36M

"Lived in"

NameTownYears
BrownLondon15
BrownYork5
JonesParis21
SmithBrussels15
SmithSantander5

Database programming

  • A) Tables are predicates composed of facts.

    :- module(_,_,[]).
    
    % The argnames package allows giving names to arguments.
    :- use_package(argnames).
    
    :- argnames
    person(   name, age,   sex).
    %     ======================
    person(  brown, 20,   male).                 
    person(  jones, 21, female).         
    person(  smith, 36,   male).                 
    
    :- argnames
    person2(   name, age,   sex).
    %      ======================
    person2( cabeza, 33,   male).    
    person2(  bueno, 39,   male).     
    person2(  jones, 21, female).   
    
    :- argnames
    lived_in( name,       town, years).
    %       ===========================
    lived_in( brown,    london,    15).
    lived_in( brown,      york,     5).
    lived_in( jones,     paris,    21).
    lived_in( smith,  brussels,    15).
    lived_in( smith, santander,     5).
  • B) Operations on tables are predicates composed of rules.

    • Projection:

      city(C) :- 
          lived_in(_,C,_).
    • Try:

      ?- city(C).
    • Alternative definition, using argnames:

      city_(C) :- 
          lived_in${town => C}.
    • Try:

      ?- city_(C).
  • C) The operations of the relational model are easily implemented as rules:

    • Union:

      all_persons(Name,Age,Sex) :- 
          person(Name,Age,Sex).
      all_persons(Name,Age,Sex) :- 
          person2(Name,Age,Sex).
    • Try:

      ?- all_persons(Name,Age,Sex).
    • Difference (note \+ not pure! But see NaF / Clarks's completion later...)

      difference(Name,Age,Sex) :- 
          person(Name,Age,Sex), 
          \+ person2(Name,Age,Sex).
      difference(Name,Age,Sex) :- 
          person2(Name,Age,Sex),
          \+ person(Name,Age,Sex).
    • Try:

      ?- difference(Name,Age,Sex).
    • Cartesian Product:

      person_X_lived_in(Name1,Age,Sex,Name2,Town,Years) :- 
          person(Name1,Age,Sex), 
          lived_in(Name2,Town,Years).
    • Try:

      ?- person_X_lived_in(Name1,Age,Sex,Name2,Town,Years).
    • Selection:

      underage_person(Name,Age,Sex) :- 
          person(Name,Age,Sex), 
          Age < 21. % We should use Peano here also of course.
    • Try:

      ?- underage_person(Name,Age,Sex).
  • D) Derived operations – some can be expressed more directly in LP:

    • Intersection:

      person_lived_in(Name,Age,Sex,Town,Years) :-
          person(Name,Age,Sex), 
          lived_in(Name,Town,Years).
    • Try:

      ?- person_lived_in(Name,Age,Sex,Town,Years).
    • Join:

      person_joinName_person2(Name,Age,Sex) :-
          person(Name,Age,Sex), 
          person2(Name,_Age2,_Sex2).
    • Try:

      ?- person_joinName_person2(Name,Age,Sex).
Duplicates? See “setof” and friends later in Prolog.

Logic Programs and the Relational DB Model (Contd.)

  • The operations of the relational model are easily implemented as rules.
    • Union:

      r_union_s(,…,) ← r(,…,).
      r_union_s(,…,) ← s(,…,).

    • Cartesian Product:

      r_X_s(,…,,,…,) ← r(,…,),s(,…,).

    • Projection:

      r13(,) ← r(,,).

    • Selection:

      r_selected(,,) ← r(,,),≤(,).
      (≤/2 can be, e.g., Peano, Prolog built-in, constraints...)

    • Set Difference:

      r_diff_s(,…,) ← r(,…,), not s(,…,).
      r_diff_s(,…,) ← s(,…,), not r(,…,).
      (we postpone the discussion on negation until later.)

  • Derived operations – some can be expressed more directly in LP:

    • Intersection:

      r_meet_s(,…,) ← r(,…,), s(,…,).

    • Join:

      r_joinX2_s(,…,) ← r(,,,…,), s(,,,…,).

  • Duplicates an issue: see "setof" later in Prolog.

Deductive Databases / Datalog / ASP

  • The subject of "deductive databases" uses these ideas to develop logic-based databases.
  • Subset of definite programs: syntactic restrictions used. E.g.,
    → Datalog: no functors (thus no data structures); no existential variables.
  • Powerful notions of negation supported: S-models
    → Answer Set Programming (ASP)
  • Variations of a "bottom-up" execution strategy used: operator (explained in the theory part) used to compute the model, restrict to the query.
  • Objective of the syntactic restrictions:
    • Systems are decidable (as Databases) – answers and failures in finite time.
    • But not Turing complete (i.e., not a programming language as Prolog).
    But powerful knowledge representation and reasoning systems!
  • sCASP system: combines Prolog-style execution with ASP!

Pure Logic Programming Examples
(Recursion, Data Types)

Computational Logic

Recursive Programming

  • Example: towards defining ancestor/2:

    parent(X,Y) :- father(X,Y).
    parent(X,Y) :- mother(X,Y). 
    
    ancestor(X,Y) :- parent(X,Y).
    ancestor(X,Y) :- parent(X,Z), parent(Z,Y).
    ancestor(X,Y) :- parent(X,Z), parent(Z,W), parent(W,Y).
    ancestor(X,Y) :- parent(X,Z), parent(Z,W), parent(W,K), parent(K,Y).
    ...
  • Defining ancestor recursively

    :- module(_,_,[]).
    
    parent(X,Y) :- father_of(X,Y).
    parent(X,Y) :- mother_of(X,Y).
    
    ancestor(X,Y) :- parent(X,Y).
    ancestor(X,Y) :- parent(X,Z), ancestor(Z,Y).
    
    father_of(john, peter).
    father_of(john, mary).
    father_of(peter, michael). 
    
    mother_of(mary, david).
    mother_of(jill, john).
  • Some queries to try:

    ?- ancestor(john,michael).
    ?- ancestor(john,elisabeth).
    ?- ancestor(john,X).
    ?- ancestor(X,elisabeth).
    ?- ancestor(X,michael).
    ?- ancestor(X,Y).
  • Exercise (from previous part): define "related", "cousin", "same generation", etc.

    exercise! ▶

What about "not equal"?

  • In the previous exercise, two people are siblings if they share a parent … and are not the same person: sibling(X,Y) :-  parent(P,X), parent(P,Y), ???
    Indeed sometimes we need to say that something does not hold.

  • As we will see later, Prolog does provide built-ins for it:
    \= /2, "not unifiable" (also \== /2, "not identical").

  • But they are not relations (they are not "pure") because they test how much their arguments happen to be instantiated at the moment they are called:

    ?- X \= Y.            yes % But X and Y may be unifiable, e.g.:
    ?- X=a, Y=a, X \= Y.  no
  • → Answer depends on when the goal is run, not only on what is true (→ "impure").
  • → We can no longer read the program declaratively: the meaning is no longer given by the clauses alone.
  • The pure alternative is the one we have been using all along:
    if we need a (negative) relation, we define it.

What about "not equal"? (Contd.)

  • In the exercise the domain is small and known (people in the database), so we can give each a number, and we can define disequality on numbers recursively:
Expressing disequality as a relation (instead of using \== or \=):

  • We assign a number to the people in our database:

    :- module(_,_,[]).
    
    person_num(john,    0).
    person_num(peter,   s(0)).
    person_num(mary,    s(s(0))).
    person_num(michael, s(s(s(0)))).
    person_num(david,   s(s(s(s(0))))).
    person_num(jill,    s(s(s(s(s(0)))))).
  • Defining disequality of (Peano) numbers:

    diffnat(0,s(_)).                    % 0 differs from any s(...)
    diffnat(s(_),0).                    % any s(...) differs from 0
    diffnat(s(X),s(Y)) :- diffnat(X,Y). % same depth: look further in
  • We can now define disequality of people:

    different_person(X,Y) :-
        person_num(X,NX),
        person_num(Y,NY),
        diffnat(NX,NY).
  • Try:

    ?- diffnat(0,s(0)).
    ?- diffnat(s(0),s(s(0))).
    ?- diffnat(s(s(0)),s(s(0))).
  • And it also enumerates! (it is a real relation):

    ?- diffnat(s(s(0)),X).
  • Now different persons:

    ?- different_person(john,mary).
    ?- different_person(john,john).
  • Which, again, also enumerates:

    ?- different_person(john,X).
    ?- different_person(X,Y).
  • Compare with the built-in, which is not a relation:

    ?- john \= mary.
    works OK, but:

    ?- X \= Y.
    produces 'no' (and does not enumerate), but X and Y could be the different! For example:

    ?- X = john, Y = mary, X \= Y.
    which does produce 'yes'.
Going back to the family database, we can use different_person/2 to write sibling/2:

father_of(john, peter).
father_of(john, mary).
father_of(peter, michael).

mother_of(mary, david).
mother_of(jill, john).

parent(X,Y) :- father_of(X,Y).
parent(X,Y) :- mother_of(X,Y).

sibling(X,Y) :- parent(P,X), parent(P,Y), different_person(X,Y).
?- sibling(X,Y).
?- sibling(peter,peter).
  • For a recursive type the same idea works structurally: two terms differ if they have different constructors, or the same one and some argument differs.

  • More work than \= /2, etc., and only for closed types (we must know all the functors); but we get a genuine relation, that works in any mode and enumerates!
  • → General negation is harder; several approaches: we return to it later.

Types

  • Type: a (possibly infinite) set of terms.

  • Type definition: A program defining a type.

An example of (non-recursive) types: dates

Example: weekday:

  • Set of terms to represent: 'Monday', 'Tuesday', Wednesday', ...
  • Type definition:

    :- module(_,_,[]).
    
    weekday('Monday').
    weekday('Tuesday').
    weekday('Wednesday').
    weekday('Thursday').
    weekday('Friday').
    weekday('Saturday').
    weekday('Sunday').
Example: day of month:
  • Set of terms to represent: 1, 2, ..., 31.
  • Type definition:

    day_of_month(1).
    day_of_month(2).
    % ...
    day_of_month(31).
Example: Date (weekday * day in the month):
  • Set of terms to represent: date(’Monday’,23), date(’Tuesday’,24), . . .
  • Type definition:

    date(date(W,D)) :- weekday(W), day_of_month(D).
Sample queries:

  • Checking if something is a date:

    ?- date(date('Monday',31)).
  • Checking something that is not a date:

    ?- date(date(a,31)).
  • Generating dates (the terms of the type):

    ?- date(D).
'Property'-based testing for free!

Recursive Programming: Recursive Types

  • Recursive types: defined by recursive logic programs.

  • Example: natural numbers (simplest recursive data type):

    • Set of terms to represent:         0, s(0),   s(s(0)),   …
    • Type definition:
      nat(0).
      nat(s(X)) :- nat(X).
    A minimal recursive predicate:
    one unit clause and one recursive clause (with a single body literal).

  • Types are runnable and can be used to check or produce values:

    • ?- nat(X) ⇒     X=0;  X=s(0);  X=s(s(0));   …
  • We can reason about complexity, for a given class of queries ("mode").
    E.g., for mode nat(ground) complexity is linear in size of number.

  • Another example: integers:

    • Set of terms to represent:        0, s(0), -s(0), s(s(0)), …
    • Type definition:
      integer( X) :- nat(X).
      integer(-X) :- nat(X).

    • 'Duplication' (of 0) is not a problem. But, can we eliminate it?

Recursive Programming: Arithmetic

Defining the order relation on natural numbers:

  • Simplest recursive type: nat/1

    % :- module(_,_,[sr/bfall]).
    :- module(_,_,[]).
    
    nat(0).
    nat(s(X)) :- nat(X).
  • The order relation:

    less_or_equal(0,X) :- nat(X).
    less_or_equal(s(X),s(Y)) :- less_or_equal(X,Y).
  • Using it:

    • Multiple uses:

      ?- less_or_equal(s(0),s(s(0))).
      ?- less_or_equal(X,0).
    • Multiple solutions:

      ?- less_or_equal(X,s(0)).
      ?- less_or_equal(s(s(0)),Y).
      plus(0,Y,Y) :- nat(Y).
      plus(s(X),Y,s(Z)) :- plus(X,Y,Z).
    • Multiple uses:

      ?- plus(s(s(0)),s(0),Z).
      ?- plus(s(s(0)),Y,s(0)).
      ?- plus(s(0),Y,s(s(s(0)))).
    • Multiple solutions:

      ?- plus(X,Y,s(s(s(0)))).

Recursive Programming: Arithmetic

  • Another possible definition of addition:

    plus(X,0,X) :- nat(X).
    plus(X,s(Y),s(Z)) :- plus(X,Y,Z).
  • The meaning of plus is the same, even if both definitions are combined.

  • Not recommended: several proof trees for the same query → not efficient, not concise. We look for minimal axiomatizations.

  • The art of logic programming: finding compact and computationally efficient formulations!

  • Try to define: times(X,Y,Z) (Z = X*Y), exp(N,X,Y) (Y = X),
    factorial(N,F) (F = N!), minimum(N1,N2,Min),

    exercise! ▶

Recursive Programming: Arithmetic

  • Defining mod(X,Y,Z): “Z is the remainder from dividing X by Y”
  • Specification: ∃Q s.t. X = Y ∗ Q + Z ∧ Z < Y

    We can simply write the specification:

    :- module(_,_,[sr/bfall]).
    
    mod(X,Y,Z) :- 
        less(Z, Y), 
        times(Y,_Q,W), 
        plus(W,Z,X).
    Try:

    ?- op(100,fy,s).
    (defines s as a prefix operator to save us writing parenthesis)

    ?- mod(s s s s s s s s s s s s s 0, s s s 0, Z).
    ?- mod(s s s s s s s s s s s s s 0, Y, s 0).
  • Another possible definition:

    mod2(X,Y,X) :- 
        less(X, Y).
    mod2(X,Y,Z) :- 
        plus(X1,Y,X), 
        mod2(X1,Y,Z).
    ?- mod2(s s s s s s s s s s s s s 0, s s s 0, Z).
    ?- mod2(s s s s s s s s s s s s s 0, Y, s 0).
    This second one is much more efficient than the first one for several query modes!
  • Auxiliary predicates used above:

    less(0,s(X)) :- nat(X).
    less(s(X),s(Y)) :- less(X,Y).
    
    plus(0,Y,Y) :- nat(Y).
    plus(s(X),Y,s(Z)) :- plus(X,Y,Z).
    
    times(0,Y,0) :- nat(Y).
    times(s(X),Y,Z) :- plus(W,Y,Z), times(X,Y,W).
    
    nat(0).
    nat(s(X)) :- nat(X).

Recursive Programming: Arithmetic/Functions

  • Functions can be coded as predicates with one more argument.
  • This extra argument represents the "output".
  • E.g., the Ackermann function:

    fun ack 0 n = n + 1
      | ack m 0 = ack (m - 1) 1
      | ack m n = ack (m - 1) (ack m (n - 1))
    Using Peano arithmetic:

    datatype nat = Zero | S of nat
    fun ack 0 n = S n
      | ack (S m1) 0 = ack m1 (S 0)
      | ack (S m1) (S n1) = ack m1 (ack (S m1) n1)
    is represented as plain clauses as follows:

    :- module(_,_,[rtchecks,modes]).
    
    ack(0,N,s(N)).
    ack(s(M),0,Val)    :- ack(M,s(0),Val).
    ack(s(M),s(N),Val) :- ack(s(M),N,Val1), ack(M,Val1,Val).
    Try:

    ?- op(100,fy,s).
    (defines s as a prefix operator to save us writing parenthesis)

    ?- ack(s s 0, s s s 0, X).
    But of course now it also runs 'backwards'!

    ?- ack(s s 0, s s s 0, s s s s s s s s s 0).
    ?- ack(s s 0, Y, s s s s s s s s s 0).
    ?- ack(X, s s s 0, s s s s s s s s s 0).
  • If we want to enforce that it can only be called as a function (i.e., all arguments input and only the last one output), we can use an assertion, for example:

    % :- pred ack(+,+,-).
    If you place this assertion above the ack predicate, and reload, you will see an error if you try to call ack/3 in another mode. This is done by the rtchecks package above.
  • Note that much syntactic sugar for functional notation is available!

Functional Syntax: Packages and Directives (I)

  • :- use_package(fsyntax).  Provides:

    • ~ "eval", which makes the last argument implicit. This allows writing, e.g.

      p(X,Y) :- q(X,Z), r(Z,Y).
      as

      p(X,Y) :- r(~q(X),Y).
      or

      p(X,~r(~q(X)). 

    • := for definitions: which allows writing, e.g.

      p(X,Y) :- q(X,Z), r(Z,Y).
      as

      p(X) := Y :- r(~q(X),Y).
      or

      p(X) := ~r(~q(X)). 

    • | for or
    • etc.

Functional Syntax: Packages and Directives (II)

  • Thus, for example, this clause:

    ack(s(M),s(N),Val) :-
             ack(s(M),N,Val1), ack(M,Val1,Val).
    can be rewritten (using also :- op(100,fy,s).) as:

    ack(s M, s N) := ~ack(M, ~ack(s M, N) ).
  • To evaluate automatically functors that are defined as functions
    (so there is no need to use ~ for them):

    :- fun_eval ack/2.
    ack(s M, s N) := ack(M, ack(s M, N) ).
  • To enable this for all functions defined in a given file:

    :- fun_eval defined(true). 
  • To evaluate arithmetic functors automatically (no need for ~ for them):

    :- fun_eval arith(true). 
    add_one(X,X+1).
  • The functional package includes fsyntax + both fun_eval's above:

    :- use_package(functional). 

Recursive Programming: Arithmetic/Functions (Functional Syntax)

  • The Ackermann function (in Peano) using Ciao's functional syntax:

    :- module(_,_,[]).
    
    :- use_package(functional).
  • We also define s as a prefix operator (saves writing parenthesis)

    :- op(100,fy,s).
  • And the function:

    ack(  0,   N) := s N.
    ack(s M,   0) := ack(M, s 0).
    ack(s M, s N) := ack(M, ack(s M, N) ).
    Try:

    ?- op(100,fy,s).
    ?- ack(s s 0, s s s 0, X).
  • But now it also runs in other modes:

    ?- ack(s s 0, s s s 0, s s s s s s s s s 0).
    ?- ack(s s 0, Y, s s s s s s s s s 0).
    ?- ack(X, s s s 0, s s s s s s s s s 0).
  • Functional syntax is convenient also e.g. for defining types:

    nat(0).
    nat(s(X)) :- nat(X).
    Using special := notation for the "return" (last) argument:

    nat := 0.
    nat := s(X) :- nat(X).

Recursive Programming: Arithmetic/Functions (Funct. Syntax, Contd.)

Moving body call to head using the ~ notation ("evaluate and replace with result"):

nat := 0.
nat := s(~nat).
~ not needed with functional package if inside a function's own definition:

nat := 0.
nat := s(nat).
Using an :- op(100,fy,s). declaration to define s as a prefix operator:

nat := 0.
nat := s nat.
Using "|" (disjunction) we get a nice "type definition:"
:- module(_,_,[functional]).

:- op(100,fy,s).

nat := 0 | s nat.
Try:

?- op(100,fy,s).
?- nat(s s 0).
?- nat(a).
?- nat(X).
Which is exactly equivalent to:

nat(0).
nat(s(X)) :- nat(X).
exercise! ▶

Recursive Programming: Lists

  • Binary structure: first argument is element, second argument is rest of the list.
  • We need:
    • A constant symbol: we use the constant    [] (→ denotes the empty list).
    • A functor of arity 2: traditionally the dot ./2 (which is overloaded).
  • Syntactic sugar: the term .(X,Y) is denoted by [X|Y]
    where X is called the head, and Y is called the tail of the list.

    Formal object "Cons pair" syntax "Element" syntax
    .(a,[])[a|[]][a]
    .(a,.(b,[]))[a|[b|[]]][a,b]
    .(a,.(b,.(c,[])))[a|[b|[c|[]]]][a,b,c]
    .(a,X)[a|X][a|X]
    .(a,.(b,X))[a|[b|X]][a,b|X]

  • Note that:

    [a,b] and [a|X] unify with {X = [b]}[a] and [a|X] unify with {X = []}
    [a] and [a,b|X] do not unify [] and [X] do not unify

Recursive Programming: Lists (Contd.)

  • Type definition (no syntactic sugar):

    list([]).
    list(.(X,Y)) :- list(Y).
  • List type definition, with some syntactic sugar ([ ] notation)

    :- module(_,_,[]).
    
    list([]).
    list([_|Y]) :-
         list(Y).
    Try:

    ?- list([a,b,c]).
    ?- list(a).
  • "Exploring" the type:

    ?- list(L).
  • List type definition, using also functional package:

    :- use_package(functional).
    
    lst := [] | [_|lst].
    Try:

    ?- lst([a,b,c]).
    ?- lst(a).
  • "Exploring" the type:

    ?- lst(L).

Recursive Programming: Lists (Contd.) – Membership in a List

  • X is a member of the list Y:

    member(a,[a]). member(b,[b]). etc.⇒ member(X,[X]).
    member(a,[a,c]). member(b,[b,d]). etc.⇒ member(X,[X,Y]).
    member(a,[a,c,d]). member(b,[b,d,l]). etc.⇒ member(X,[X,Y,Z]).

    ⇒ member(X,[XY]) :- list(Y).

    member(a,[c,a]), member(b,[d,b]). etc.⇒ member(X,[Y,X]).
    member(a,[c,d,a]). member(b,[s,t,b]). etc.⇒ member(X,[Y,Z,X]).

    ⇒ member(X,[YZ]) :- member(X,Z).

  • Resulting definition:
:- module(_,_,[]).
% :- use_package(sr/bfall).

% list_member(X,L): X is a member of list L
list_member(X,[X|L]) :-
    list(L).
list_member(X,[_|T]) :- 
    list_member(X,T).

list([]).
list([_|T]) :-
     list(T).
  • Uses of member/2:

    • Checking whether an element is in a list:

      ?- member(b,[a,b,c]).
    • Finding an element in a list:

      ?- member(X,[a,b,c]).
    • Generating a list containing a given element:

      ?- member(a,Y).

Recursive Programming: Lists (Contd.) – Length, Sum, Typed Lists

Combining lists and naturals

  • list_length(L,N): N is the length of list L.

    :- module(_,_,[]).
    % :- use_package(sr/bfall).
    
    list_length([],0).
    list_length([_|T],s(N)) :-
        list_length(T,N).
    Try:

    ?- list_length([a, b, c], N).
    ?- list_length(L, s(s(s(0)))).
    ?- list_length(L, 3).
    ?- list_length(L, N).
  • sumlist(L,N): N is the sum of all elements of L.

    sumlist([],0).
    sumlist([H|T],S) :-
        sumlist(T,ST),
        add(ST,H,S).
    Try:

    ?- sumlist([s(0),s(s(s(0))),s(s(0))],N).
    ?- sumlist(L,s(s(s(0)))).
    ?- sumlist(L,S).
  • natlist(L): L is a list of naturals.

    natlist([]).
    natlist([X|Y]) :- 
        nat(X),
        natlist(Y).
    Or, using functional notation:

    :- use_package(fsyntax).
    natlst := [] | [~nat|~natlst].
Try also uncommenting :- use_package(sr/bfall). above.

Auxiliary predicates:

list([]).
list([_|T]) :-
     list(T).

add(0,X,X) :- 
     nat(X).
add(s(X),Y,s(Z)) :- 
     add(X,Y,Z).

nat(0).
nat(s(X)) :- 
     nat(X).

Recursive Programming: One Way to Understand It

  • One way to visualize recursion is as creating 'copies' of procedures:
?- sumlist([s(0), s(s(0)), s(0)], R).
         \
          sumlist([],0).       % fails
          sumlist([H|T],S) :-  % H=s(0), T=[s(s(0)), s(0)]
              sumlist(T,ST),   % T=[s(s(0)), s(0)]
              |      \                                     
              |       sumlist([],0).       % fails
              |       sumlist([H1|T1],S1) :-  % H1=s(s(0), T1=[s(0)]
              |          sumlist(T1,ST1),     % T1=[s(0)] 
              |          |       \                                  
              |          |        sumlist([],0).          % fails
              |          |        sumlist([H2|T2],S2) :-  % H2=s(0), T2=[]
              |          |           sumlist(T2,ST2),     % T2=[]
              |          |           |      \                                    
              |          |           |       sumlist([],0).      % ST2=0
              |          |           |      /                              
              |          |           % H2=s(0), ST2=0
              |          |           plus(H2,ST2,S2). % S2=s(0)
              |          |          /                                                      
              |          % H1=s(s(0)), ST1=s(0)
              |          plus(H1,ST1,S1). % S1=s(s(s(0)))
              |         /                                
              % H=s(0), ST=s(s(s(0)
              plus(H,ST,S). % S=s(s(s(s(0))))
             /
            % R=s(s(s(s(0))))

Recursive Programming: Lists (Contd.) – List Concatenation

  • Concatenation of lists:
    • Base case:
      append([], [a], [a]).
      append([], [a,b], [a,b]). etc.
      ⇒ append([], Ys, Ys) :- list(Ys).
    • Rest of cases (first step):
      append([a], [b], [a,b]).
      append([a], [b,c], [a,b,c]). etc.
      ⇒ append([X], Ys, [X|Ys]) :- list(Ys).
      append([a,b], [c], [a,b,c]).
      append([a,b], [c,d], [a,b,c,d]). etc.
      ⇒ append([X,Z], Ys, [X,Z|Ys]) :- list(Ys).

      This is still infinite → we need to generalize more.

Recursive Programming: Lists (Contd.) – List Concatenation

  • Second generalization:
    append([X],Ys,[X|Ys]) :- list(Ys).
    append([X,Z],Ys,[X,Z|Ys]) :- list(Ys).
    append([X,Z,W],Ys,[X,Z,W|Ys]) :- list(Ys).
    ⇒ append([X|Xs],Ys,[X|Zs]) :- append(Xs,Ys,Zs).
  • So, we have:

    append([],Ys,Ys) :- list(Ys).
    append([X|Xs],Ys,[X|Zs]) :- append(Xs,Ys,Zs).
  • Another way of reasoning – thinking inductively:
    • The base case is: append([],Ys,Ys) :- list(Ys).
    • If we assume that append(Xs,Ys,Zs) works for some iteration, then, in the next one, the following should hold: append([X|Xs],Ys,[X|Zs]).

Recursive Programming: Lists (Contd.) – List Concatenation

Uses of append/3

:- module(_,_,[]).

% `append(X,Y,Z)`: list `Z` is `X` and `Y` concatenated.
list_append([],L,L) :- 
    list(L).
list_append([X|Xs],Ys,[X|Zs]) :- 
    list_append(Xs,Ys,Zs).

list([]).
list([_|Y]) :-
     list(Y).
  • Concatenate two given lists:

    ?- list_append([a,b], [c], X).
  • Find differences between lists:

    ?- list_append(X, [c], [a,b,c]).
  • Splitting a list:

    ?- list_append(X, Y, [a,b,c]).

Recursive Programming: Lists (Contd.) – Two Ways of Reversing a List

  • Objective: reverse(Xs,Ys) where Ys is list Xs reversed.
  • First version - basically the definition (thinking recursively):
    • An empty list reversed is the empty list (base case):

      :- module(_,_,[]).
      
      reverse_naive([],[]).
    • Inductive step: if we assume Xs is already reversed as Zs, to reverse [X|Xs] (one more element at the beginning), it has to go at the end of Zs.

      reverse_naive([X|Xs],Ys) :- 
          reverse_naive(Xs,Zs),
          list_append(Zs,[X],Ys).
    Try:

    ?- reverse_naive([a,b,c],X).
    ?- reverse_naive(X,[c,b,a]).
    ?- reverse_naive(X,Y).
  • As defined, reverse(Xs,Ys) is very inefficient. Another possible definition (using an accumulating parameter):

    reverse_acc(Xs,Ys) :- 
        reverse_(Xs,[],Ys).
    
    reverse_([],Ys,Ys).
    reverse_([X|Xs],Acc,Ys) :-
        NewAcc=[X|Acc],
        reverse_(Xs,NewAcc,Ys).
    ?- reverse_acc([a,b,c],X).
    ?- reverse_acc(X,[c,b,a]).
    ?- reverse_acc(X,Y).
Auxiliary predicates for the above:

% append(X,Y,Z): list `Z` is `X` and `Y` concatenated.
list_append([],L,L) :- 
    list(L).
list_append([X|Xs],Ys,[X|Zs]) :- 
    list_append(Xs,Ys,Zs).

list([]).
list([_|Y]) :-
     list(Y).
  • ⇒ Find the differences in terms of efficiency between the two definitions. exercise! ▶

Recursive Programming: Lists (Contd.)

  • Some additional exercises on lists. Define:

    exercise! ▶

    • prefix(X,Y), where the list X is a prefix of the list Y, e.g.,
      prefix([a, b], [a, b, c, d])
    • suffix(X,Y), where the list X is a suffix of the list Y, e.g.,
      suffix([c, d], [a, b, c, d]).
    • sublist(X,Y), where the list X is a sublist of list Y, e.g.,
      sublist([b, c], [a, b, c, d]).
      (Do it in various ways!)
    • select(L,X,NL), where NL is L minus one occurrence of X, e.g.,
      select([a, b, c, b], b, [a, b, c]).
    • delete(L,X,NL), where NL is L minus all occurrences of X, e.g.,
      delete([a, b, c, b], b, [a, c]).
    • permutation(X,Y), where list Y is a permutation of list X, e.g.,
      permutation([a, b, c], [c, b, a]),
    • etc.!

Recursive Programming: Binary Trees

  • Represented by a ternary functor tree(Element,Left,Right).
  • Empty tree represented by void.
  • Defining the type binary tree and some operations:

    :- module(_,_,[doccomments]).
    % :- use_package(sr/bfall).
    
    %! binary_tree(T): `T` is a binary tree.
    binary_tree(void).
    binary_tree(tree(_Element,Left,Right)) :- 
        binary_tree(Left),
        binary_tree(Right).
    • Seeing if a tree is indeed a tree (i.e., of the type):

      ?- tree_example(T), binary_tree(T).
      ?- tree_example(_T), binary_tree(_T).
    • Checking something that is not in the type:

      ?- binary_tree(tree(a, b, c)).
      ?- binary_tree(tree(a, [], [])).
    • Generating binary trees:

      ?- binary_tree(T).
    • Try also uncommenting the :- use_package(sr/bfall). above!
  • Same, using functional notation:

    :- use_package(functional).
    
    %! binary_t(T): `T` is a binary tree.
    binary_t := void | tree(_Element, ~binary_t, ~binary_t). 
  • Defining tree membership:

    %! tree_member(X,T): `X` is a node in binary tree `T`.
    tree_member(X,tree(X,Left,Right)) :- 
        binary_tree(Left),
        binary_tree(Right).
    tree_member(X,tree(_,Left,_Right)) :- 
        tree_member(X,Left). 
    tree_member(X,tree(_,_Left,Right)) :- 
        tree_member(X,Right).
    • Try:

      ?- tree_example(_T), tree_member(b, _T).
      ?- tree_example(_T), tree_member(X, _T).
      ?- tree_example(_T), tree_member(e, _T).
      ?- tree_member(e,T).
      ?- tree_member(e,T), tree_member(a,T).
    • Try also uncommenting the :- use_package(sr/bfall). above!
  • Same, using functional notation:

    tree_mem_f(X) := tree( X, ~binary_tree,   ~binary_tree)
                   | tree(_Y, ~tree_mem_f(X), _Right)
                   | tree(_Y, _Left,          ~tree_mem_f(X)).
    • Try:

      ?- tree_example(_T), tree_mem_f(b, _T).
      ?- tree_example(_T), tree_mem_f(X, _T).
      ?- tree_example(_T), tree_mem_f(e, _T).
      ?- tree_member(e,T).
      ?- tree_member(e,T), tree_mem_f(a,T).
    • Try also uncommenting the :- use_package(sr/bfall). above!
  • Auxiliary predicate - a tree example:

    %! tree_example(T): `T` is a tree example.
    tree_example(  tree( a,
                       tree( b,
                             void,
                             void
                           ),
                       tree( c,
                             tree( b,
                                   void,
                                   void
                                   ),
                             void
                           )
                       )).

Recursive Programming: Binary Trees

  • Defining pre_order(Tree,Elements):
    Elements is a list containing the elements of Tree traversed in preorder.
:- module(_,_,[doccomments]).

%! \title Binary tree example: preorder

%! pre_order(T,L): `L` is the list of elements of tree `T` in preorder. 
pre_order(void, []).
pre_order(tree(X,Left,Right), Elements) :-
    pre_order(Left, ElementsLeft),
    pre_order(Right, ElementsRight),
    list_append([X|ElementsLeft], ElementsRight, Elements).
  • Try:

    ?- tree_example(_T),pre_order(_T,List).
  • Auxiliary predicates used above:

    list_append([],L,L) :- 
        list(L).
    list_append([X|Xs],Ys,[X|Zs]) :- 
        list_append(Xs,Ys,Zs).
    
    tree_example(  tree( a,
                       tree( b,
                             void,
                             void
                           ),
                       tree( c,
                             tree( b,
                                   void,
                                   void
                                   ),
                             void
                           )
                       )).
exercise! ▶

  • Exercise – define:
    • in_order(Tree,Elements)
    • post_order(Tree,Elements)

Polymorphism

  • Note that the two definitions of member/2 can be used simultaneously:
:- module(_,_,[doccomments,sr/bfall]).

%! \title Polymorphic member

%! \module We can write a 'polymorphic' member that works 
% with both lists and trees. 

%! lt_member(X,LT): X is a member of the list or tree LT.

lt_member(X, [X|Y]) :- list(Y).
lt_member(X, [_|T]) :- lt_member(X, T).

lt_member(X, tree(X, L, R)) :- binary_tree(L), binary_tree(R).
lt_member(X, tree(_Y, L, _R)) :- lt_member(X, L). 
lt_member(X, tree(_Y, _L, R)) :- lt_member(X, R).
  • Lists only unify with the first two clauses, trees with clauses 3–5!
  • Try:

    ?- lt_member(M,[1,2,3]).
    ?- tree_example(_T), lt_member(M,_T).
    However:

    ?- lt_member(M,T).
    We also get elements that are of mixed type –because that is what we specified!
  • If we want to separate the types:

    %! lt_member_separate(X,LT): X is a member of the list or tree LT.
    lt_member_separate(X, Y) :- list_member(X, Y).
    lt_member_separate(X, Y) :- tree_member(X, Y).
    Try:

    ?- lt_member_separate(M,[1,2,3]).
    ?- tree_example(_T), lt_member_separate(M,_T).
    And:

    ?- lt_member_separate(M,T).
    Now we obtain separated types!
  • Auxiliary predicates:

    :- redefining(list/1).
    
    list([]).
    list([_|Y]) :-
         list(Y).
    
    list_member(X,[X|L]) :-
        list(L).
    list_member(X,[_|T]) :- 
        list_member(X,T).
    
    tree_member(X,tree(X,Left,Right)) :- 
        binary_tree(Left),
        binary_tree(Right).
    tree_member(X,tree(_,Left,_Right)) :- 
        tree_member(X,Left). 
    tree_member(X,tree(_,_Left,Right)) :- 
        tree_member(X,Right).
    
    binary_tree(void).
    binary_tree(tree(_Element,Left,Right)) :- 
        binary_tree(Left),
        binary_tree(Right).
    
    tree_example(  tree( a,
                       tree( b,
                             void,
                             void
                           ),
                       tree( c,
                             tree( b,
                                   void,
                                   void
                                   ),
                             void
                           )
                       )).

Recursive Programming: Manipulating Symbolic Expressions

  • Recognizing (and generating!) polynomials in some term X:
    • X is a polynomial in X
    • a constant is a polynomial in X
    • sums, differences and products of polynomials in X are polynomials
    • also polynomials raised to the power of a natural number and the quotient of a polynomial by a constant
  • polynomial(T,X): T is a polynomial in X. E.g.: a * x ^ s(s(0)) + b

    :- module(_,_,[sr/bfall]).
    
    polynomial(X,X).
    polynomial(Term,_X)  :- 
        pconstant(Term).
    polynomial(Term1+Term2,X)  :- 
        polynomial(Term1,X),
        polynomial(Term2,X). 
    polynomial(Term1-Term2,X)  :- 
        polynomial(Term1,X), 
        polynomial(Term2,X).
    polynomial(Term1*Term2,X)  :- 
        polynomial(Term1,X), 
        polynomial(Term2,X).
    polynomial(Term1/Term2,X)  :- 
        polynomial(Term1,X), 
        pconstant(Term2).
    polynomial(Term1^N,X)  :- 
        polynomial(Term1,X), 
        nat_num(N).
    
    pconstant(X) :- 
        nat_num(X).
    pconstant(a).
    pconstant(b).
    pconstant(c).
    Try:

    ?- polynomial( a * x ^ s(s(0)) + b, x).
    ?- polynomial(P, x).
    ?- polynomial( a * x ^ x + b, x).
  • Version using functional notation (note that arguments are reversed in this case!):

    :- use_package(fsyntax).
    poly_f(X) := X
        | ~pconst_f
        | ~poly_f(X) + ~poly_f(X)
        | ~poly_f(X) - ~poly_f(X)
        | ~poly_f(X) * ~poly_f(X)
        | ~poly_f(X) / ~pconst_f
        | ~poly_f(X) ^ ~nat_num. 
    
    pconst_f := ~nat_num | a | b | c.
    Try:

    ?- poly_f(x, a * x ^ s(s(0)) + b).
    ?- poly_f(x, P).
    ?- poly_f(x, a * x ^ x + b ).
  • Auxiliary predicates:

    nat_num(0).
    nat_num(s(X)) :- 
        nat_num(X).

Recursive Programming: Manipulating Symb. Expressions (Contd.)

  • Symbolic differentiation deriv(Expression, X, Derivative)

    :- module(_,_,[sr/bfall]).
    
    %! deriv(E,X,D): `D` is the derivative of expression `E` w.r.t. `X`.
    
    deriv(X,     X, s(0)                 ).
    deriv(C,    _X, 0                    ) :- pconstant(C).
    deriv(U+V,   X, DU+DV                ) :- deriv(U,X,DU), deriv(V,X,DV). 
    deriv(U-V,   X, DU-DV                ) :- deriv(U,X,DU), deriv(V,X,DV).
    deriv(U*V,   X, DU*V+U*DV            ) :- deriv(U,X,DU), deriv(V,X,DV).
    deriv(U/V,   X, (DU*V-U*DV)/V^s(s(0))) :- deriv(U,X,DU), deriv(V,X,DV).
    deriv(U^s(N),X, s(N)*U^N*DU          ) :- deriv(U,X,DU), nat_num(N).
    deriv(log(U),X, DU/U                 ) :- deriv(U,X,DU).
    
    pconstant(X) :- nat_num(X).
    pconstant(a).
    pconstant(b).
    pconstant(c).
    • Try:

      ?- deriv(s(s(s(0)))*x+s(s(0)),x,Y).
      ?- deriv(s(s(s(0)))*x+s(s(0)),x,0*x+s(s(s(0)))*s(0)+0).
      ?- deriv(E,x,0*x+s(s(s(0)))*s(0)+0).
    (You can ask for several solutions)
  • Version using functional notation:

    :- use_package(fsyntax).
    derivf(X,          X) := s(0).
    derivf(~pconstant, _) := 0.
    derivf(U+V,        X) := ~derivf(U,X) + ~derivf(V,X).
    derivf(U-V,        X) := ~derivf(U,X) - ~derivf(V,X).
    derivf(U*V,        X) := ~derivf(U,X) * V  +  U * ~derivf(V,X).
    derivf(U/V,        X) := ( ~derivf(U,X) * V - U * ~derivf(V,X) ) / V^s(s(0)).
    derivf(U^s(N),     X) := s(N) * U^N * ~derivf(U,X) :- nat_num(N).
    derivf(log(U),     X) := ~derivf(U,X) / U.
    • Try:

      ?- derivf(s(s(s(0)))*x+s(s(0)),x,Y).
      ?- derivf(s(s(s(0)))*x+s(s(0)),x,0*x+s(s(s(0)))*s(0)+0).
      ?- derivf(E,x,0*x+s(s(s(0)))*s(0)+0).
  • Auxiliary predicates:

    nat_num(0).
    nat_num(s(X)) :- 
        nat_num(X).
  • A simplification step can be added to eliminate superfluous terms like 0*x, s(0) in s(0)*x, etc. Not totally trivial but can be fun; the enclosed exercise provides some guidance for writing your own simplifier:

    exercise! ▶

Recursive Programming: Graphs

  • Directed, acyclic graphs (fact representation)

    • Example (directed, acyclic graph):

      a ----> b             f
      |       |             |
      v       v             v
      c ----> d ----> e     g
              |
              v
              h
      edge_da(a,b).
      edge_da(a,c).
      edge_da(b,d).
      edge_da(c,d).
      edge_da(d,e).
      edge_da(d,h).
      edge_da(f,g).
    • path_da(X,Y): There is a path from X to Y (transitive closure of the edge relation).

      path_da(X,Y) :-
          edge_da(X,Y).
      path_da(X,Y) :-
          edge_da(X,Z),
          path_da(Z,Y).
      
      circuit_da :- path_da(A,A).
    • Try:

      ?- path_da(a,e).
      yes
      ?- path_da(a,g).
      no
      Nodes that c is connected to:

      ?- path_da(c,X).
      All connected nodes:

      ?- path_da(X,Y).
      (We get duplicate solutions for different paths.)
    • Note that more needed if graph is not directed.
    • Also if it has cycles.
  • Directed, acyclic graphs (list representation)

    • Same example, in list format:

      graph_da([
          edge(a,b),
          edge(a,c),
          edge(b,d),
          edge(c,d),
          edge(d,e),
          edge(d,h),
          edge(f,g) ]).
      
      path_l(X,Y,G) :-
          member(edge(X,Y),G).
      path_l(X,Y,G) :-
          member(edge(X,Z),G),
          path_l(Z,Y,G).
    • Try:

      ?- graph_da(_G), path_l(a,e,_G).
      yes
      ?- graph_da(_G), path_l(a,g,_G).
      no
      Nodes that c is connected to:

      ?- graph_da(_G), path_l(c,X,_G).
      All connected nodes:

      ?- graph_da(_G), path_l(X,Y,_G).
      (We get duplicate solutions for different paths.)
  • Undirected graphs

    path_a(X,Y) :-
          connected_a(X,Y).
    path_a(X,Y) :-
          connected_a(X,Z),
          path_a(Z,Y).
    
    connected_a(X,Y) :- edge_da(X,Y).
    connected_a(Y,X) :- edge_da(X,Y).
    
    circuit_a :- path_a(A,A).
    • Try:

      ?- path_a(a,e).
      yes
      Nodes that c is conn to - but duplicates (infinite):

      ?- path_a(c,X).
      Also duplicates (infinite):

      ?- path_a(X,Y).
      Loops:

      ?- path_a(a,g).
    • We need to deal with cycles!
  • Dealing with cycles (list representation)

    • Example (directed graph, with cycle):

      a <---- b             f
      |       ^             |
      v       |             v
      c ----> d ----> e     g
              |
              v
              h
    • List representation:

      graph([
          edge(a,c),
          edge(c,d),
          edge(d,b),
          edge(b,a),
          edge(d,e),
          edge(d,h),
          edge(f,g) ]).
    • The code:

      path(X,Y,G) :-
          member(edge(X,Y),G).
      path(X,Y,G) :-
          list_select(edge(X,Z),G,GRest),
          path(Z,Y,GRest).
      
      % `list_select(X,L,NL)`: selects an element `X` out of list `L`
      % and `NL` is `L` without `X`.
      list_select(X, [X|Xs], Xs).
      list_select(X, [Y|Ys], [Y|Zs]) :-
           list_select(X, Ys, Zs).
    • Try:

      ?- graph(_G), path(a,e,_G).
      ?- graph(_G), path(a,g,_G).
      ?- graph(_G), path(c,X,_G).
      ?- graph(_G), path(X,Y,_G).

Recursive Programming: Graphs (Exercises)

exercise! ▶

  • Modify path/2 so that it takes an identifier of the circuit as an argument.
  • Modify path/2 and circuit/0 so that they return the list of nodes in the path.
  • Write all the previous predicates for the different cases of representing graphs as a data structure (e.g., list of pairs of nodes, etc.) and as facts.
  • In your experiments, try unconnected graphs (there is a subset of nodes not connected in any way to the rest) versus connected graphs.
  • Try also undirected graphs, add support for them.
  • Consider the issues with graphs with cycles, and how to deal with them.
    E.g., in the previous graph, try path(a,d) (solve the problem).

Recursive Programming: Automata (Graphs)

  • Recognizing the sequence of characters accepted by the following
    non-deterministic, finite automaton (NDFA):

    where q0 is both the initial and the final state.

  • We will represent the strings as lists of constants (e.g., [a,b,b]).
  • Definition of a (generic) non-deterministic, finite automaton:

    :- module(_,_,[sr/bfall]).
    
    accept(S) :- initial(Q), accept_from(S, Q).
    
    accept_from([],Q)     :- final(Q).
    accept_from([C|Cs],Q) :- delta(Q, C, NewQ), accept_from(Cs,NewQ).
  • A concrete automaton, defined by its initial and final states:

    initial(q0).      final(q0).
    and its transitions:

    delta(q0, a, q1). 
    delta(q1, b, q0). 
    delta(q1, b, q1).
             a
       -------------
       |           |   ___
    ___|__      ___v__/   \
    | q0 |      | q1 |     | b
    ------      ------<    |
       ^           |   \__/
       |           |
       -------------
             b
  • Check if these sequences accepted:

    ?- accept([a,b,b]).
    ?- accept([a,b,b,a]).
  • List all sequences of 4 chars:

    ?- accept([A, B, C, D]).
  • List all accepted sequences:

    ?- accept(X).

Recursive Programming: Automata (Graphs) (Contd.)

  • A Nondeterministic PushDown Automaton (NPDA, automaton with stack):

  • Definition of (generic) Non-deterministic PushDown Automaton (NPDA, automaton w/stack):

    :- module(_,_,[sr/bfall]).
    
    npda_accept(S) :- 
        npda_initial(Q),
        npda_accept_from(S, Q, []).
    
    npda_accept_from([], Q, [])  :-
        npda_final(Q).
    npda_accept_from([C|Cs], Q, Stack) :-  
        npda_delta(Q, C, Stack, NewQ, NStack),
        npda_accept_from(Cs, NewQ, NStack).
    • A concrete automaton, defined by its initial and final states:

      npda_initial(q0).     
      npda_final(q1).
      and its transitions:

      npda_delta(q0, C,     Stack, q0, [C|Stack]).
      npda_delta(q0, C,     Stack, q1, [C|Stack]).
      npda_delta(q0,_C,     Stack, q1,    Stack ).
      npda_delta(q1, C, [C|Stack], q1,    Stack ).
    • Check if these sequences accepted:

      ?- npda_accept([a,b,b,a]).
      ?- npda_accept([a,b,c,b,a]).
      ?- npda_accept([a,b,c,c,b,a]).
      ?- npda_accept([a,b,X,a]).
    • List all sequences of 4 chars:

      ?- npda_accept([A, B, C, D]).
    • List all accepted sequences:

      ?- npda_accept(X).
    • What is the name of the sequences it recognizes?
  • Alternative: accept only words formed with symbols of a particular alphabet.

    npda_accept_alt(S) :- 
        npda_initial_alt(Q),
        npda_accept_from_alt(S, Q, []).
    
    npda_accept_from_alt([], Q, [])  :-
        npda_final_alt(Q).
    npda_accept_from_alt([C|Cs], Q, Stack) :-  
        npda_delta_alt(Q, C, Stack, NewQ, NStack),
        npda_accept_from_alt(Cs, NewQ, NStack).
    • A concrete automaton, defined by its initial and final states:

      npda_initial_alt(q0).     
      npda_final_alt(q1).
      
      npda_delta_alt(q0, C,     Stack, q0, [C|Stack]) :- symbol(C).
      npda_delta_alt(q0, C,     Stack, q1, [C|Stack]) :- symbol(C).
      npda_delta_alt(q0, C,     Stack, q1,    Stack ) :- symbol(C).
      npda_delta_alt(q1, C, [C|Stack], q1,    Stack ) :- symbol(C).
      
      symbol(a).
      symbol(b).
      symbol(c).
    • Check if these sequences accepted:

      ?- npda_accept_alt([a,b,b,a]).
      ?- npda_accept_alt([a,b,c,b,a]).
      ?- npda_accept_alt([a,b,c,c,b,a]).
      ?- npda_accept_alt([a,b,X,a]).
    • List all sequences of 4 chars:

      ?- npda_accept_alt([A, B, C, D]).
    • List all accepted sequences:

      ?- npda_accept_alt(X).

Recursive Programming: Towers of Hanoi

  • Objective:
    • Move tower of N disks from peg a to peg b, with the help of peg c.
  • Rules:
    • Only one disk can be moved at a time.
    • A larger disk can never be placed on top of a smaller disk.

Recursive Programming: Towers of Hanoi (Representation)

  • We will call the main predicate hanoi_moves(N,Moves)
  • N is the number of disks and Moves the corresponding list of "moves".
  • Each move move(A, B) represents that the top disk in A should be moved to B.
  • Example: The moves for three disks

    are represented by:

    hanoi_moves( s(s(s(0))), 
                 [ move(a,b), move(a,c), move(b,c), move(a,b),
                   move(c,a), move(c,b), move(a,b) ])

Recursive Programming: Towers of Hanoi (Solution)

  • A general rule:

    To move N disks from peg A to peg B using peg C we need to:

    move N-1 disks to peg C using peg B, move the bottom disk to peg B, and then move the N-1 disks from peg C to peg B using peg A.

  • We capture this in a predicate hanoi(N,Orig,Dest,Help,Moves) where

    "Moves contains the moves needed to move a tower of N disks from peg Orig to peg Dest, with the help of peg Help."

  • Towers of Hanoi: Move N disks from peg A to peg B using peg C.

    :- module(_,_,[]).
    
    hanoi_moves(N,Moves) :-
          hanoi(N,a,b,c,Moves).
    
    hanoi(s(0),A,B,_,[move(A, B)]).
    hanoi(s(N),A,B,C,Moves) :- 
        hanoi(N,A,C,B,Moves1),
        hanoi(N,C,B,A,Moves2), 
        list_append(Moves1,[move(A, B)|Moves2],Moves).
    Try:

    ?- hanoi_moves(s(s(s(0))),M).
    ?- hanoi_moves(s(s(s(0))),M), hanoi_moves(N,M).
    ?- hanoi_moves(N,M).
  • Checking execution time:

    ?- hanoi_test(NDisks).
    and hit Next for increasing numbers of disks. Execution time is exponential in the number of disks!
  • Auxiliary code for performance:

    hanoi_test(D) :- 
        between(1,1000,D), 
        decimal_peano(D,N), 
        statistics(walltime, [_,_]),
        hanoi_moves(N,M),
        statistics(walltime, [_,T]),
        length(M,L),
        write(D), write(' disks = '),
        write(L), write(' moves in '),
        write(T), write(' mS').
    
    :- use_module(engine(runtime_control),[statistics/2]).
    :- use_module(library(write),[write/1]).
    :- use_module(library(lists),[length/2]).
    :- use_module(library(between)).
    
    list_append([],L,L) :- 
        list(L).
    list_append([X|Xs],Ys,[X|Zs]) :- 
        list_append(Xs,Ys,Zs).
    
    list([]).
    list([_|Y]) :-
        list(Y).
    
    decimal_peano(0,0).
    decimal_peano(N,s(X)) :-
        N>0,
        NN is N-1,
        decimal_peano(NN,X).

Learning to Compose Recursive Programs

  • To some extent it is a simple question of practice.
  • By generalization (as in the previous examples): elegant, but sometimes difficult? (Not the way most people do it.)
  • Think inductively: state first the base case(s), and then think about the general recursive case(s).
  • Sometimes it may help to compose programs with a given use in mind (e.g., "forwards execution"), making sure it is declaratively correct. Consider then also if alternative uses make sense.
  • Sometimes it helps to look at well-written examples and use the same "schemas."
  • Using a global top-down design approach can help
    (in general, not just for recursive programs):

    • State the general problem.
    • Break it down into subproblems.
    • Solve the pieces.
  • Again, the best approach: practice, practice, practice.