☰

Additional Features of Prolog
and the ISO Standard

Computational Logic


The ISO Prolog standard

  • Prolog, as the most popular logic programming language is standardized by ISO.
  • The standard includes many constructs beyond the "pure" Prolog seen so far:

    • many useful pre-defined predicates ( some not declarative, for efficiency),
    • higher-order and meta-logical capabilities,
    • program modification, ...
    Also, establishes depth-first search rule and left-to-right control rule by default.
  • Advantages:

    • more expressive power,
    • industry standard,
    • mature implementations with modules, graphical environments, interfaces, ...
  • Drawbacks of "classical" systems (and how addressed by modern systems):

    • "Depth-first search rule is efficient but can lead to incompleteness"
      → alternative search strategies (e.g., Ciao's bfall, tabling, id, etc.).
    • "No occur check in unification (which led to unsoundness in older systems)"
      → support regular (i.e., infinite) trees: X = f(X) (already constraint-LP).
      → switchable occur check.

An Overview of The ISO Prolog Standard

  • Syntax (incl. operators) and operational semantics
  • Arithmetic
  • Checking basic types and state
  • Structure inspection and term comparison
  • Input/Output
  • Pruning operators (cut)
  • Meta-calls, higher-order, aggregation predicates
  • Negation as failure, cut-fail
  • Dynamic program modification
  • Meta-interpreters
  • Incomplete data structures
  • Exception handling
Additionally (recently in standard):
  • Definite Clause Grammars (DCGs): parsing

Programming Interface (Writing and Running Programs)

  • Not specified in the ISO-Prolog language standard.
  • → Left to each particular system implementing the standard.

  • This typically includes issues such as:
    • User interaction (top-level, GUI, etc.).
    • Interpreter(s).
    • Compiler(s).
    • Debugger(s).
    • Module system.
  • See the part on Developing Programs with a Logic Programming System
    for more details for the particular system used in the course (Ciao).

(ISO-)Prolog Syntax and Terminology

  • Variables, constants, functors, etc. as before.

    Note: in Prolog terminology constants are also referred to as "atoms."

  • Some new data types and syntax:

    • Numbers: 0, 999, -77, 5.23, 0.23e-5, 0.23E-5.

      Infinite precision integers supported by many current systems (e.g., Ciao).

    • Strings (of "codes"): "Prolog" = [80,114,111,108,111,103]
      (list of ASCII –also, e.g., utf8– character codes).
      • Important note: if we set ?- set_prolog_flag(write_strings, on).
        lists of character codes will be printed as strings: " ", very useful!
    • Comments:
      • Using %: rest of line is a comment.
      • Using /* ... */: everything in between is a comment.

Prolog Syntax — Defining Operators

  • Certain functors and predicate symbols are predefined as infix, prefix, or postfix operators, aside from the standard term notation, and new ones can be added.
  • Very useful to make programs/data files more readable! → Language extensions.
  • Stated using operator declarations:
    :- op(, , ).

    • : is an integer from 1 to 1200.

      General rule: the operator with the highest precedence number is the principal functor.
      E.g., if ‘+’ has higher precedence number than ‘/’, then
      a+b/c ≡ a+(b/c) ≡ +(a,/(b,c)).
      Alternatively, we can always use parenthesis: /(+(a,b),c) ≡ (a+b)/c

      (Note that in some other languages the ordering of precedence values is the opposite.)

    • :
      • infix: xfx (not associative), xfy (right associative), yfx (left associative).
      • prefix: fx (non-associative), fy (associative).
      • postfix: xf (non-associative), yf (associative).
    • : can be a single atom or a list of atoms.

Prolog Syntax — Operators (Contd.)

  • Examples:

    Standard Notation Operator Notation
    +(a,/(b,c))a+b/c
    is(X, mod(34, 7))X is 34 mod 7
    <(+(3,4),8)3+4 < 8
    =(X,f(Y))X = f(Y)
    -(3)-3
    spy(/(foo,3))spy foo/3
    :-(p(X),q(Y))p(X) :- q(Y)
    :-(p(X),','(q(Y),r(Z)))p(X) :- q(Y),r(Z)

  • Note that, with this syntax convention, Prolog clauses are also Prolog terms!
  • Parenthesis must always be used for operators with higher priority than 1000
    (i.e., the priority of comma: ,): ..., assert( (p :- q) ), ...
  • Operators are by default local to modules (explained later).
Operator example:

  • If we include this declaration in a file:

    :- module(_,_,[]).
    
    %          v------- Infix operator, no associativity
    :- op(500,xfx,is_father_of).
    %     ^---- Precedence = 500
    then these two facts:

    %     is_father_of(john, peter).
    %     is_father_of(peter,jim).
    can be written as:

    john  is_father_of  peter. 
    peter is_father_of  jim.
    Try:

    ?- is_father_of(X,Y).
  • To activate the operator in the top level:

    ?- op(500,xfx,is_father_of).
    (this happens by default if module/2 is used). Now we can do:

    ?- X is_father_of Y.
  • Another example for defining facts that are instructions of an assembly language (just the syntax):

    :- op(1100,fx,[load,mult,bne]).
    :- op( 500,fx,@).
    :- op(1200,xfx,:).
    
    label1 : load @r3,r1.
             load 5,r2.
             mult r1,r2.
             bne  0,label1.

Prolog Syntax — Operators (Contd.)

  • Typical standard operators:
:- op( 1200, xfx, [ :-, --> ]).
:- op( 1200,  fx, [ :-, ?- ]).
:- op( 1150,  fx, [ dynamic, multifile, meta_predicate]).
:- op( 1100, xfy, [ ; ]).
:- op( 1050, xfy, [ -> ]).
:- op( 1000, xfy, [ ',' ]).
:- op(  900,  fy, [ \+, spy, nospy ]).
:- op(  700, xfx, [ =, \=, is, =.., ==, \==, @<, @>, @=<, 
                    @>=, =:=, =\=, <, >, =<, >= ]).
:- op(  550, xfx, [ : ]).
:- op(  500, yfx, [ +, -, #, /\, \/ ]).
:- op(  400, yfx, [ *, /, //, rem, mod, <<, >> ]).
:- op(  200,  fy, [ +, -, \ ]).
:- op(  200, xfx, [ ** ]).
:- op(  200, xfy, [ ^ ]).

The Execution Mechanism of (classical) Prolog

  • Always execute calls in the body of clauses left-to-right.
  • When entering a procedure, if several clauses unify (a choice point), take the first unifying clause (i.e., the leftmost unexplored branch).
  • On failure, backtrack to the next unexplored clause of the last choice point.
  • Simple example for following Prolog execution rules / search tree / debugger:

    :- module(_, _).
    
    grandparent(C,G) :-
        parent(C,P),
        parent(P,G).
    
    parent(C,P) :- 
        father(C,P).
    parent(C,P) :- 
        mother(C,P).
    
    father(charles, philip).
    father(ana, george).
    
    mother(charles, ana).
  • Try:

    ?- grandparent(charles,X).
  • Run it in the debugger (C-c d, or debug button) to follow the execution step by step!

Built-in Arithmetic

  • Practicality: interface to the underlying CPU arithmetic capabilities.
  • These arithmetic operations are not as general as their logical counterparts.
  • Interface: evaluator of arithmetic terms.
  • The type of arithmetic terms (arithexpr/1 – in next slide):
    • a number is an arithmetic term,
    • if is an -ary arithmetic functor and are arithmetic terms then is an arithmetic term.
  • Arithmetic functors: +, -, *, / (float quotient), // (integer quotient), mod, ... (see later).
    Examples:

    • (3*X+Y)/Z, correct if when evaluated X, Y and Z are arithmetic terms, otherwise it will raise an error.
    • a+3*X raises an error (because a is not an arithmetic term).

Built-in Arithmetic (Contd.) – The arithexpression type

(Note: the type is arithexpression, but we use just arithexpr below to save space.)

arithexpr :=
   | num
   | + arithexpr            | - arithexpr
   | ++ arithexpr           | -- arithexpr
   | arithexpr + arithexpr  | arithexpr - arithexpr
   | arithexpr * arithexpr  | arithexpr // arithexpr
   | arithexpr / arithexpr  | abs(arithexpr)
   | sign(arithexpr)        | float_integer_part(arithexpr)
   | float(arithexpr)       | float_fractional_part(arithexpr)
   | arithexpr ** arithexpr | exp(arithexpr)
   | log(arithexpr)         | sqrt(arithexpr)
   | sin(arithexpr)         | cos(arithexpr)
   | atan(arithexpr)        | [arithexpr]
   | $\ldots$
Other arithmetic operators that can appear in arithmetic expressions (see manuals):

  • rem, mod, gcd, >>, <<, /\, \/, \\, #, ...
  • integer, truncate, floor, round, ceiling, ...

Built-in Arithmetic (Contd.)

  • Built-in arithmetic predicates:

    • Z is X
      X (which must be an arithmetic term) is evaluated and result is unified with Z.
    • the usual <, >, =<, >=, =:= (arithmetic equal), =\= (arithmetic not equal), ...
      Both arguments are evaluated (as in is/2) and their results are compared.
  • Examples:

    ISO-Prolog arithmetic

    • Examples of success:

      Try:

      ?- X is 3*3//2.
      ?- Z = 3//2, X is 3+Z.
    • Examples of failure (fails and the system will backtrack):

      Try:

      ?- X=3, Y=4, Y < X+1.
      ?- X=3, Y=4, X is Y+1.
      ?- X=3, Y=4, X =:= Y.
    • Examples of error (throw error and the system will abort):

      Try:

      ?- X is 3+Y.
      ?- Y=4, Y < a+1.
      ?- X is Z+1.
      ?- X=3, X =:= f(a).

Arithmetic Programs

  • We can define plus(X,Y,Z) :-   Z is X +  Y.

    • But it only works in one mode (X and Y bound to arithmetic terms).
    • In exchange, we have won (a lot) in performance!
    Meta-logical tests and constraints (see later) will allow using arithmetic in more modes.

  • Comparing Peano arithmetic and ISO-Prolog is/2 built-in for computing factorial

    • a) Using Peano arithmetic:

      :- module(_,_).
      
      peano_factorial(0,s(0)).
      peano_factorial(s(N),F) :-
          peano_factorial(N,F1),
          peano_times(s(N),F1,F).
      
      peano_times(0,_,0).
      peano_times(s(X),Y,Z) :-
          peano_times(X,Y,W),
          peano_plus(W,Y,Z).
      
      peano_plus(0,Y,Y) :-
          nat(Y).
      peano_plus(s(X),Y,s(Z)) :-
          peano_plus(X,Y,Z).
      
      nat(0).
      nat(s(X)) :- nat(X).
      ?- peano_factorial(s(s(s(s(s(0))))),F).
      ?- peano_factorial(s(s(s(s(s(0))))),_).
      ?- peano_factorial(s(s(s(s(s(s(0)))))),_).
      ?- peano_factorial(s(s(s(s(s(s(s(0))))))),_).
      Takes long...
    • b) Using ISO-Prolog is/2 and >/2:

      factorial(0,1). 
      factorial(N,F) :-
          N > 0,
          N1 is N-1,
          factorial(N1,F1),
          F is F1*N.
      Try:

      ?- factorial(7,F).
      ?- factorial(1000,F).
      ?- factorial(100000,_).
      But:

      ?- factorial(N,6).
      Error!
    • c) Wrong goal order can raise an error (e.g., moving the last call to is/2 to before the call to factorial):

      wrong_factorial(0,1). 
      wrong_factorial(N,F) :-
          N > 0,
          N1 is N-1,
          F is F1*N,
          wrong_factorial(N1,F1).
      Try:

      ?- wrong_factorial(3,F).
      Error!
  • → Exercise: write version with accumulating parameter (tail recursive). exercise! ▶

Dynamic Checking of Basic Types

  • Unary relations which check the type of a term:
    • integer(X)
    • float(X)
    • number(X)
    • atom(X) (nonvariable term of arity 0 other than a number)
    • atomic(X) atom or number
    • ...
  • They behave as if defined by a (possibly infinite) table of facts (in part, see below).
  • They either succeed or fail, but do not produce an error.
  • Thus, they cannot be used to generate (e.g., if argument is a variable, they fail instead of instantiating it to possible values).
  • This behaviour is outside first order logic because it allows checking the instantiation state of a variable.

Type Checking Predicates (Contd.)

  • Example: implementing a better behavior for plus/3:

    Recovering some reversibility for the ISO-Prolog is/2 built-in

    :- module(_,_).
    
    plus_plain(X,Y,Z) :- Z is X + Y.
    Try:

    ?- plus_plain(4,5,Z).
    Works, but cannot 'run backwards':

    ?- plus_plain(X,5,9).
    To recover some reversibility:

    plus(X,Y,Z) :- number(X),number(Y), Z is X + Y.
    plus(X,Y,Z) :- number(X),number(Z), Y is Z - X.
    plus(X,Y,Z) :- number(Y),number(Z), X is Z - Y.
    Try:

    ?- plus(4,5,Z).
    ?- plus(X,5,9).
    But, it cannot do:

    ?- plus(X,Y,9).
    In fact, this should raise an error, rather than simply failing (we will see solutions for this later).

    • A much better solution: the addition of constraints to the language: Constraint Logic Programming (CLP) –see later!

Structure Inspection

  • functor(X, F, A):

    • X is a term f(X1,...,Xn) → F=f   A=
    • F is the atom f and A is the integer →  X = f(X1,..,Xn)
    • Some errors: if X, and either F or A are variables;
      or A is not an integer or less than 0; etc.
    • Fails if unification of an output with given value fails.
    Examples:

    • Structure Inspection: Examples for functor/3

      Try:

      ?- functor(t(b,a),F,A).
      ?- functor(Term,f,3).
      ?- functor(Vector,v,100).
    (Note: in some systems functor arity is limited to 256 by default;
    there are libraries that allow unbounded arrays.)

Structure Inspection (Contd.)

  • arg(N, X, Arg):

    • N integer, X compound term → Arg unified with N-th argument of X.
    • Allows accessing a structure argument in constant time and in a compact way.
    • Error if N not integer, N<0, X not a compound, or X a free variable.
    • Fails for N=0, or N>=Arity, or if unification fails.
    Examples:

    • Structure Inspection: Examples for arg/3 (and functor/3):

      Try:

      ?- _T=date(9,February,1947), arg(3,_T,X).
    • Same, using unification:

      ?- _T=date(9,February,1947), _T=date(_,_,X).
      The _ in _T instructs top level not to print the value of _T

      ?- functor(Array,array,5), arg(1,Array,black), arg(5,Array,white).
    • What does the following return? Explain why!

      ?- arg(2, [a,b,c,d], X).

Example of Structure Inspection: Arrays

  • Define add_arrays(A,B,C), an "array" addition using functor/3 and arg/3.

    The explanations in the comments in the code assume that we call with mode add_arrays(+,+,?), i.e., we provide the first two arrays and obtain the sum. Step through it in the debugger to see its operation!

    :- module(_,_).
    % Example of Structure Inspection: Arrays
    
    add_arrays(A,B,C):-
        functor(A,array,N), % Gets length N from array A
        functor(B,array,N), % Checks that array B is of same length
        functor(C,array,N), % Creates an array C to hold the result
        add_elements(N,A,B,C).
    
    add_elements(0,_A,_B,_C).
    add_elements(I,A,B,C):-
        I>0, 
        arg(I,A,AI), 
        arg(I,B,BI), 
        arg(I,C,CI),
        CI is AI + BI, 
        I1 is I - 1,
        add_elements(I1,A,B,C).
    Try:

    ?- add_arrays(array(1,2,3),array(4,5,6),R).
    ?- add_arrays(array(1,2,3),array(4,5,6),array(5,7,9)).
    ?- add_arrays(array(1,2,3),array(4,5),R).
  • Alternative, using lists instead of structures:

    add_lists([],[],[]).
    add_lists([X|Xs],[Y|Ys],[Z|Zs]):-
        Z is X + Y,
        add_lists(Xs,Ys,Zs).
    Try:

    ?- add_lists([1,2,3],[4,5,6],R).
    ?- add_lists([1,2,3],[4,5,6],[5,7,9]).
    ?- add_lists([1,2,3],[4,5],R).
  • In the latter case, where do we check that the three lists are of equal length?

Example of Structure Inspection: Arrays (Contd.)

Alternative 'array' addition using functor/3 and arg/3, but defining some syntactic sugar for arg/3, to be used in add_els/4.

  • First, we define some syntax.

    Allow functional notation:

    :- module(_,_).
    % Example of Structure Inspection: Arrays, with some syntax 
    
    :- use_package(fsyntax).  
    Make @ be an infix operator:

    :- op(250,xfx,@).         
    Define @ as a call to arg/3:

    T @ N := A :- arg(N,T,A).   
          % Same as:  @(T,N) := A :- arg(N,T,A).
          % or also:  @(T,N,A)    :- arg(N,T,A).
    Turn on evaluation of all occurrences of @/2:

    :- fun_eval @ / 2.        
    Define array/2 to hide the call to functor:

    array(A,N) :- functor(A,array,N).
    Turn on evaluation of arith. expressions (e.g., I-1):

    :- fun_eval arith(true).  
  • Now we can define array addition as:

    add_arr(A,B,C) :-
        array(A,N), 
        array(B,N), 
        array(C,N),
        add_els(N,A,B,C).
    
    add_els(0,_,_,_).
    add_els(I,A,B,C) :-
        I>0, 
        C@I is A@I + B@I,   % <- Using our definition of @/2
        add_els(I-1,A,B,C).
  • Try:

    ?- add_arr( array(1,2,3), array(4,5,6), R).
    ?- add_arr( array(1,2,3), array(4,5,6), array(5,7,9)).
    ?- add_arr( array(1,2,3), array(4,5),R).
    (Should fail - or, better, raise error.)
  • Much more is possible with similar syntactic extensions!

    Example: Prime numbers up to N, using Eratosthenes sieve.

    (Coded using the functional, loops, and arrays packages)

    :- module(_, _, [functional,loops,arrays]).
    :- use_module(library(arrays/arrays_log)).
    
    primes(N) := Res :-
        % Initialize the array with n+1 values to access until index n
        A = ~new_array_log,
        for (I in 2 .. N) { A[I] := true },
        % We cross out the non primes
        for (I in 2 .. ~floor(sqrt(N))) { % Loop goes until square root of N
            if (A[I] == true) { % If a[i] is prime we cross out its multiples
                for (J in I*I .. I .. N) { A[J] := false }
            }
        },
        % We take the primes from the sieve and put them into a list
        Res = ResTail,
        for (K in 2 .. N) {
            if (A[K] == true) { accum(!ResTail,K) }
        },
        ResTail = [].
    
    accum(!R, X) :- R = [X|Tail], R := Tail.
    Try:

    ?- primes(10, Ps).
    ?- primes(100, Ps).
    ?- primes(10000, _L), length(_L,MPrimes).

Example of Structure Inspection: Subterms of a Term

  • Define subterm(Sub,Term) (Sub is a subterm of Term):

    :- module(_,_).
    % Example of Structure Inspection: Subterm
    
    subterm(Term,Term).    % a) A term is always a subterm of itself
    subterm(Sub,Term):-    % b) The arguments are also subterms:
        nonvar(Term),      %    - Check Term not a free variable
        functor(Term,_F,N),%    - N is the number of arguments of Term
        n_to_one(N, J),    %    - J is a natural between N and 1
        arg(J,Term,Arg),   %    - Arg is the J-th argument of Term
        subterm(Sub,Arg).  %    - Sub are the subterms of Arg
    
    n_to_one(N, N) :- N > 0.
    n_to_one(N, X) :- N > 1, N1 is N-1, n_to_one(N1, X).
  • Try (asking also for other solutions):

    ?- n_to_one( 4, N ).
    ?- subterm( f(a) , g(b,f(a)) ).
    ?- subterm( f(b) , g(b,f(a)) ).
    ?- subterm( g(b,f(a)) , g(b,f(a)) ).
    ?- subterm( X , g(b,f(a)) ).
    ?- subterm( f(X) , g(b,f(a)) ).
    ?- subterm( X , g(X,f(a)) ).
    ?- subterm( f(X) , g(b,f(X)) ).

'Higher-Order' Structure Inspection

  • T =.. L    (read as "univ")

    • L is the decomposition of a term T into a list comprising its principal functor followed by its arguments.

      Try:

      ?- date(9,february,1947) =.. L.
      ?- T =.. [date,9,february,1947].
      ?- _F = '+', X =.. [_F,a,b].
      (The _ in _F instructs top level not to print the value of _F.)
    • Exercise: write univ with arg/3 and functor/3 and vice-versa. exercise! ▶
    • Useful for implementing higher-order primitives (see later).
    • Example -- extending derivative:

      :- module(_,_).
      % Extending derivative: 
      
      deriv(sin(X), X, cos(X)). 
      deriv(cos(X), X, -sin(X)). 
      % Chain rule: 
      deriv(FG_X,   X, DF_G * DG_X):-
          FG_X =.. [_, G_X],
          deriv(FG_X, G_X, DF_G),
          deriv(G_X, X, DG_X).
      Try:

      ?- deriv(sin(cos(x)),x,D).

Conversion Between Strings and Atoms (New Atom Creation)

  • Classical primitive: name(A,S)

    Conversion between strings and atoms

    • A is the atom or number whose name is the list of ASCII characters S in name(A,S):

      Try:

      ?- name(hello,S).
      ?- name(A,[104,101,108,108,111]).
      ?- name(A,"hello").
      ?- set_prolog_flag(write_strings,on).
      ?- name(hello,S).
    • Ambiguity when converting strings which represent numbers:

      Try:

      ?- X='1', atom(X), name(X,S), name(Y,S), number(Y).
      (Succeeds! Note that we start with the atom '1' and we end up with the number 1.)
    • Thus, the ISO standard provides also the non-ambiguous atom_codes/2 and number_codes/2:

      Try:

      ?- atom_codes('1', L).
      ?- atom_codes(1, L).
      ?- number_codes('1', L).
      ?- number_codes(1, L).
      ?- number_codes(X, "1").
      ?- atom_codes(X, "1").

Meta-Logical Predicates

  • var(X): succeeds iff X is a free variable.
    ?- var(X), X = f(a).  % Succeeds
    ?- X = f(a), var(X).  % Fails
  • nonvar(X): succeeds iff X is not a free variable.
    ?- X = f(Y), nonvar(X).  % Succeeds
  • compound(X): succeeds iff X is instantiated to a compound term.
    ?- X = f(Y), compound(X).  %  Succeeds
  • ground(X): succeeds iff X is fully instantiated.
    ?- X = f(Y), ground(X).  %  Fails
  • Outside the scope of first order logic.
  • Uses:

    • control goal order,
    • restore some flexibility to programs using certain builtins.

Meta-Logical Predicates (Contd.) – choosing implementations

  • Example: list length

    Choosing between two implementations based on calling mode, using meta-logical predicates (i.e., implementing reversibility "by hand").

    • A) Simple list length implementation (non tail-recursive):

      :- module(_,_).
      
      len([],0).
      len([_|T],N) :-
          len(T,TN),
          N is TN+1.
    • B) Choosing between two implementations based on calling mode:

      This allows us to write a better (tail recursive) version for create_list/2.

      First, detect the mode and choose implementation:

      mylength(Xs,N):- 
          var(Xs),
          integer(N),
          create_list(N,Xs).
      mylength(Xs,N):- 
          nonvar(Xs),
          compute_length(Xs,N).
      Create a list of length N (tail recursive):

      create_list(0,[]).
      create_list(N,[_|Xs]):- 
          N > 0,
          N1 is N - 1,
          create_list(N1,Xs).
      Compute the length of the list (same as len/2 for now):

      compute_length([],0).
      compute_length([_|T],N):-
          compute_length(T,TN),
          N is TN+1.
      Try:

      ?- len([a,b,c],N).
      ?- len(L,3).
      ?- mylength([a,b,c],N).
      ?- mylength(L,3).
    • The complex mylength/2 version is not strictly needed, since the normal definition of length is actually reversible (although it needs Peano or constraint arithmetic to terminate after the first solution, and create_list/2 does not).
    • Also, note that len/2 when called with L a variable and N a number is less efficient than create_list/2 (which is tail-recursive).

Meta-Logical Predicates (Contd.) – choosing implementations

  • Example (Contd.):

    Choosing between two implementations based on calling mode, using meta-logical predicates (i.e., implementing reversibility "by hand").

    • C) Now also with better version of compute_length/2 using an accumulating parameter. Now both create_list/2 and compute_length/2 are tail recursive.

      First, detect the mode and choose implementation:

      :- module(_,_).
      
      mylength(Xs,N):- 
          var(Xs),
          integer(N),
          create_list(N,Xs).
      mylength(Xs,N):- 
          nonvar(Xs),
          compute_length(Xs,N).
      Create a list of length N (tail recursive):

      create_list(0,[]).
      create_list(N,[_|Xs]):- 
          N > 0,
          N1 is N - 1,
          create_list(N1,Xs).
      Compute the length of the list (now also tail recursive):

      compute_length(L,N) :-
          compute_length_(L,0,N).
      
      compute_length_([],N,N).
      compute_length_([_|T],A,N) :-
          NA is A+1,
          compute_length_(T,NA,N).
      Try:

      ?- mylength([a,b,c],N).
      ?- mylength(L,3).

Comparing Non-ground Terms

  • Many applications need comparisons between non–ground/non–numeric terms.
  • Identity tests:
    • X == Y (identical)
    • X \== Y (not identical)
    ?- f(X) == f(X).  % Succeeds
    ?- f(X) == f(Y).  % Fails
  • Term ordering:
    • X @> Y, X @>= Y, X @< Y, X @=< Y (alphabetic/lexicographic order)
    ?- f(a) @> f(b).  % Fails
    ?- f(b) @> f(a).  % Succeeds
    ?- f(X) @> f(Y).  % Implementation dependent!

Comparing Non-ground Terms (Contd.)

  • Reconsider subterm/2 with non-ground terms:

    • subterm_ng(Sub,Term): Sub is a strict subterm of Term (does not instantiate variables):

      :- module(_,_).
      % Example application of comparing non-ground terms
      % (use of ==/2, \==/2, @>/2, ...)
      
      subterm_ng(Sub,Term) :- % a) A term is a subterm of another 
          Sub == Term.        %    if they are identical.
      subterm_ng(Sub,Term) :- % b) The arguments are also subterms:
          nonvar(Term),       %    Check Term not a free variable
          functor(Term,_F,N), %    N is number of arguments of Term
          n_to_one(N, J),     %    J is a natural between N and 1
          arg(J,Term,Arg),    %    Arg is the J-th argument of Term
          subterm_ng(Sub,Arg).%    Sub are the subterms of Arg
      
      n_to_one(N, N) :- N > 0.
      n_to_one(N, X) :- N > 1, N1 is N-1, n_to_one(N1, X).
      Compare to the subterm/2 version with =/2 instead of ==/2 (below).

      Try (asking also for other solutions):

      ?- subterm_ng( f(a), g(b,f(a)) ).
      ?- subterm(    f(a), g(b,f(a)) ).
      ?- subterm_ng( f(b), g(b,f(a)) ).
      ?- subterm(    f(b), g(b,f(a)) ).
      ?- subterm_ng( g(b,f(a)), g(b,f(a)) ).
      ?- subterm(    g(b,f(a)), g(b,f(a)) ).
      ?- subterm_ng( X, g(b,f(a)) ).
      ?- subterm(    X, g(b,f(a)) ).
      ?- subterm_ng( f(X), g(b,f(a)) ).
      ?- subterm(    f(X), g(b,f(a)) ).
      ?- subterm_ng( f(a), g(b,f(X)) ).
      ?- subterm(    f(a), g(b,f(X)) ).
      ?- subterm_ng( f(X), g(b,f(X)) ).
      ?- subterm(    f(X), g(b,f(X)) ).
      ?- subterm_ng( X, g(X,f(a)) ).
      ?- subterm(    X, g(X,f(a)) ).
      % Version using =/2:
      subterm(Term,Term).  % a) A term is always a subterm of itself
      subterm(Sub,Term):-  % b) The arguments are also subterms:
          nonvar(Term),    %    Check Term not a free variable
          functor(Term,_F,N),%  N is the number of arguments of Term
          n_to_one(N, J),  %    J is a natural between N and 1
          arg(J,Term,Arg), %    Arg is the J-th argument of Term
          subterm(Sub,Arg).%    Sub are the subterms of Arg
  • Insert an item into an ordered list:

    • Version with unification:

      :- module(_,_).
      % Example application of comparing non-ground terms
      % (use of ==/2, \==/2, @>/2, ...)
      % Inserting an item into an ordered list.
      
      insert_unif([], Item, [Item]).
      insert_unif([H|T], Item, [H|T]) :- H = Item.
      insert_unif([H|T], Item, [Item, H|T]) :- H @> Item.
      insert_unif([H|T], Item, [H|NewT]) :- H @< Item, insert_unif(T, Item, NewT).
      Try:

      ?- insert_unif([], a, L).
      ?- insert_unif([a,b,e], c, L).
      ?- insert_unif([a,b,e], X, L).
    • Version with ==/2, @>/2, @</2:

      insert_ee([], Item, [Item]).
      insert_ee([H|T], Item, [H|T]) :- H == Item.
      insert_ee([H|T], Item, [Item, H|T]) :- H @> Item.
      insert_ee([H|T], Item, [H|NewT]) :- H @< Item, insert_ee(T, Item, NewT).
      Try:

      ?- insert_ee([], a, L).
      ?- insert_ee([a,b,e], c, L).
      ?- insert_ee([a,b,e], X, L).

Input/Output

  • A minimal set of input-output predicates ("DEC-10 Prolog I/O"):

Class Predicate Explanation
I/O stream control see(File)File becomes the current input stream.
seeing(File)The current input stream is File.
seenClose the current input stream.
tell(File)File becomes the current output stream.
telling(File)The current output stream is File.
toldClose the current output stream.
Term I/O write(X)Write the term X on the current output stream.
nlStart a new line on the current output stream.
read(X)Read a term (finished by a full stop) from the current input stream and unify it with X.
Character I/O put_code(N)Write the ASCII character code N. N can be a string of length one.
get_code(N)Read the next character code and unify its ASCII code with N.

  • Examples of DEC-10 I/O

    • Try:

      ?- tell(foo), write( f(g(a,[1,2,3])) ), told.
      We use the auxiliary predicate below to see the exact contents of foo:

      ?- set_prolog_flag(write_strings,on).
      ?- show_file(foo).
    • An example predicate, write_list_to_file/2:

      write_list_to_file(L,F) :- 
          telling(OldOutput),             % Grab current output stream.
          tell(F), write_list(L), told,   % Write into F, close.
          tell(OldOutput).                % Reset previous output stream.
      
      write_list([]).
      write_list([X|Xs]):-
          write(X),
          % write_canonical(X),
          write('.'),
          nl,
          write_list(Xs).
      Try:

      ?- write_list_to_file([ f(a), m, g(k)],foo).
      ?- show_file(foo).
      ?- see(foo), read(X), seen.
      ?- see(foo).
      ?- read(X).
      ?- read(X).
      ?- read(X).
      ?- seen.
    • We use this auxiliary predicate and the library to print the exact contents of files:

      :- use_module(library(stream_utils)).
      show_file(F) :- 
          file_to_string(F,L),
          format("~s",[L]).

Input/Output (Contd.)

  • Other stream-based input-output predicates:

Class Predicate Explanation
I/O stream control open(File,M,S)Open File with mode M and return in S the stream associated with the file. M may be read, write or append.
close(S)Close the stream ‘Stream’.
Term I/O write(S,X)Write the term X on stream S.
nl(S)Start a new line on stream S.
read(S,X)Read a term (finished by a full stop) from the stream S and unify it with X.
Character I/O put_code(S,N)Write the ASCII character code N on stream S.
get_code(S,N)Read from stream S the next character code and unify its ASCII code with N.

Input/Output (Contd.)

  • Example:

    write_list_to_file(L,F) :- 
        telling(OldOutput),      % Grab current output stream.
        tell(F),                 % Write into F.
        write_list(L),           % Write a list of terms.
        told,                    % Close.
        tell(OldOutput).         % Reset previous output stream.
    
    write_list([]).
    write_list([X|Xs]):- write(X), nl, write_list(Xs).
  • More powerful and format-based input-output predicates are available (see, e.g., format/2 and format/3 –Prolog system manuals).
  • All these input-output predicates are "side-effects"!

Pruning Operator: Cut

  • The "cut" (!/0) is a predicate which, when executed, commits Prolog to all the choices made since the current call to the predicate in which the cut is executed.
  • Thus, when it is executed, a cut prunes:

    • all clauses below the clause in which the executed cut appears, and
    • all alternative solutions to the goals in the clause to the left of the executed cut.
    It does not affect the search in the goals to the right of the cut.
  • Example of cut execution:

    Execute in debugger to see how execution proceeds!

    :- module(_,_).
    :- discontiguous([s/1, l/1, r/1, t/1]). 
    
    main(A,X,Y) :- s(A), p(X,Y).
    
    s(1) :- noop.     l(a) :- noop.
    s(2) :- noop.     l(b) :- noop.
    
    r(8) :- noop.     t(10) :- noop.
    r(9) :- noop.     t(11) :- noop.
    
    m(e) :- noop.     noop.
    
    p(X,_Y) :- l(X).
    p(X, Y) :- r(X), !, t(Y).
    p(X,_Y) :- m(X).
    If execution reaches the cut (!):
    • The second alternative of r/1 is not considered.
    • The third clause of p/2 is not considered.
    Try (asking for more solutions):

    ?- p(9,Y).
    ?- p(e,Y).
    ?- main(A,X,Y).
    ?- main(2,X,Y).

Pruning Operator: Cut (Contd.)

s(1).          p(X,Y):- l(X), ...                  r(8).
s(2).          p(X,Y):- r(X), !, ...               r(9).
               p(X,Y):- m(X), ...

Types of Cut: White and Green Cuts

  • White cuts: do not discard solutions. They affect neither completeness nor correctness – use them freely. (In many cases the system "introduces" them automatically.)

    :- module(_,_).
    % Types of cuts - White and Green Cuts
    % ------------------------------------
    
    % Example: 
    white_max(X,Y,X) :-
        X > Y,
        !.
    white_max(X,Y,Y) :-
        X =< Y.
    Try:

    ?- white_max(5,2,M).
    ?- white_max(2,5,M).
    ?- white_max(5,2,2).
  • Green cuts: discard correct solutions which are not needed. They affect completeness but not correctness. Can be used if we do not need all the solutions. Necessary in some situations (but be careful!).

    address(X,Addr) :-
        home_address(X,Addr), 
        !.
    address(X,Addr) :-
        business_address(X,Addr).
    
    home_address(john,pfluggerville).
    business_address(john,austin).
    business_address(mary,boston).
    Try (look for all solutions):

    ?- address(john,Addr).
    ?- address(mary,Addr).
    Another example:

    member_normal(X,[X|_]).
    member_normal(X,[_|T]) :-
        member_normal(X,T).
    
    member_check(X,[X|_]) :- !.
    member_check(X,[_|T]) :-
        member_check(X,T).
    Try (look for all solutions):

    ?- member_normal(X,[1,2,3,4]).
    Multiple answers.

    ?- member_check(X,[1,2,3,4]).
    Only one answer.

    ?- member_normal(1,[1,2,1,4]), write('found a solution'), nl, fail.
    Note that it succeeds twice.

    ?- member_check(1,[1,2,1,4]), write('found a solution'), nl, fail.
    Note that it succeeds once.

Types of Cut: Red Cuts

  • Red cuts: discard solutions which are incorrect according to intended meaning.

    But they can easily affect correctness - avoid when possible.

    • Example:

      :- module(_,_).
      % Types of cuts - Red Cuts
      % ------------------------------------
      
      red_max(X,Y,X) :-
          X > Y,
          !. % Red cut!
      red_max(_X,Y,Y). % :-  
          % X =< Y. % Missing!
      Try:

      ?- red_max(5,2,M).
      ?- red_max(2,5,M).
      ?- red_max(5,2,2).
      Note that this last one produces a wrong answer! Often because the program was written with only one mode in mind: max(+,+,-).

      Compare to (white cut):

      white_max(X,Y,X) :-
          X > Y,
          !.
      white_max(X,Y,Y) :-
          X =< Y.
      Try:

      ?- white_max(5,2,M).
      ?- white_max(2,5,M).
      ?- white_max(5,2,2).
    • Another example:

      days_in_year( X,366):-
          leap_year(X),
          !.
      days_in_year(_X,365). 
      
      leap_year(X):-
          number(X),
          0 is X mod 4.
      Some good answers:

      ?- days_in_year(4,D).
      ?- days_in_year(3,D).
      ?- days_in_year(3,366).
      ?- days_in_year(4,366).
      But wrong answers to:

      ?- days_in_year(4,365).
      ?- days_in_year(Y,366).
Red cuts affect completeness and one can no longer rely on the declarative meaning of the program to reason about correctness – avoid when possible.

Types of Cut: Red Cuts (Contd.)

  • Useful tips regarding red cuts:

    • Can be improved by delaying output bindings to after the cut:

      :- module(_,_).
      % Types of cuts - Red Cuts
      % Useful tips regarding red cuts
      % ------------------------------
      
      red_max_delay_output(X,Y,M) :-
          X > Y,
          !,
          M = X.
      red_max_delay_output(_X,Y,Y).
          % X =< Y.
      Try:

      ?- red_max_delay_output(5,2,M).
      ?- red_max_delay_output(2,5,M).
      ?- red_max_delay_output(5,2,2).
      Now also correct answer to the last one (no).
    • Using if-then-else (syntactic sugar over cut) can often be a better solution:

      if_then_else_max(X,Y,M) :- ( X>Y ->  M=X ;   M=Y).
      (if X>Y then M=X else M=Y)

      Try:

      ?- if_then_else_max(5,2,M).
      ?- if_then_else_max(2,5,M).
      ?- if_then_else_max(5,2,2).
      Also correct answer to the last one (no).

Types of Cut: Red Cuts (Contd.)

  • Useful tips regarding red cuts (leap year example):

    • The leap year example:

      :- module(_,_).
      % Types of cuts - Red Cuts
      % ------------------------
      
      days_in_year(Y,366) :-
          leap_year(Y),
          !.
      days_in_year(_Y,365).
      
      leap_year(Y) :-
          number(Y),
          0 is Y mod 4.
      Try:

      ?- days_in_year(4,D).
      ?- days_in_year(3,D).
      ?- days_in_year(3,366).
      ?- days_in_year(4,366).
      ?- days_in_year(4,365).
      ?- days_in_year(Y,366).
      The last two produce a wrong answer...
    • The best solution: making the cut white by defining the other condition:

      days_in_year_good(Y,D) :-
          leap_year(Y),
          !,
          D = 366.
      days_in_year_good(Y,D) :- 
          standard_year(Y),
          D = 365.
      
      standard_year(Y) :-
          number(Y),
          R is Y mod 4,
          R \= 0.
      Try:

      ?- days_in_year_good(4,D).
      ?- days_in_year_good(3,D).
      ?- days_in_year_good(3,366).
      ?- days_in_year_good(4,366).
      ?- days_in_year_good(4,365).
      ?- days_in_year_good(Y,366).
      ?- days_in_year_good(a,D).
    • Otherwise, if we still want to skip the call to standard_year/1 using the cut, we can also delay the output bindings to after the cut:

      days_in_year_delay_output(Y,D) :-
          leap_year(Y),
          !,
          D = 366.
      days_in_year_delay_output(_Y,365).
      Try:

      ?- days_in_year_delay_output(4,D).
      ?- days_in_year_delay_output(4,365).
      ?- days_in_year_delay_output(4,366).
      ?- days_in_year_delay_output(Y,366).
      Note that the last one still produces a wrong answer! This is because we are probably thinking of a 'mode': that we provide the year and ask for the number of days.
    • Improved solution: check that we are in the 'expected mode':

      days_in_year_moded(Y,_D) :-
          var(Y),
          !,
          write('{ ERROR: the year must be bound. }\n'),
          abort.
      days_in_year_moded(Y,D) :-
          leap_year(Y),
          !,
          D = 366.
      days_in_year_moded(_Y,365).
      Try:

      ?- days_in_year_moded(4,D).
      ?- days_in_year_moded(3,D).
      ?- days_in_year_moded(4,365).
      ?- days_in_year_moded(4,366).
      ?- days_in_year_moded(Y,366).
      ?- days_in_year_moded(a,D).
    • Even better: ensure mode and type of input and output with an assertion.

      Load assertions and modes packages + run-time checking of assertions:

      :- use_package([assertions,modes,rtchecks]).
      Use an assertion to state the expected mode:

      :- pred days_in_year_moded_assrt(+int,-int). 
      % Or equivalently, without using modes:
      % :- pred days_in_year_moded_assrt(Y,D) : (int(Y),var(D)).
      
      days_in_year_moded_assrt(Y,D) :-
          leap_year(Y),
          !,
          D=366.
      days_in_year_moded_assrt(_Y,365).
      Try:

      ?- days_in_year_moded_assrt(4,D).
      ?- days_in_year_moded_assrt(4,366).
      ?- days_in_year_moded_assrt(4,365).
      ?- days_in_year_moded_assrt(Y,366).
      ?- days_in_year_moded_assrt(a,D).
  • Do some exercises on cuts: exercise! ▶

Meta-Calls and Implementing Higher Order

  • The meta-call call(X) converts a term X into a goal and calls it.
  • When called, X must be instantiated to a term, otherwise an error is reported.
  • Used for meta-programming, such as interpreters and shells.
    Also for defining negation (as we will see) and implementing higher order.
  • Examples:

    • call/1 example: simple call

      :- module(_,_).
      % Meta-calls (call/1) and implementing apply
      % ------------------------------------------
      
      r(X) :- call(X). 
      
      q(a,1).
      q(b,2).
      Try (ask also for more solutions):

      ?- G = q(a,Y), r(G).
      ?- G = member(Y,[1,2,3]), r(G).
    • call/1 example: constructing apply (implementing higher-order)

      apply(P,Args) :-
          G =.. [P|Args],
          call(G).
      Try:

      ?- P = q, apply(P,[a,Y]).
      ?- P = q, apply(P,[X,Y]).
    • Another example: mapping a predicate onto a list of tuples

      maptuples(_P,[]).
      maptuples(P,[Tuple|RTuples]) :-
          apply(P,Tuple),
          maptuples(P,RTuples).
      Try:

      ?- maptuples(q,[[a,X],[b,Y]]).
      ?- maptuples(append, [ [ [a],[c,d], X ], [ X,  [e,f], Y ], [ X,  Y,     Z ]  ]).

Further Higher Order Facilities

  • There is also a family of predicates call/1, call/2, ..., call/n (loaded by package hiord).

    • The first argument can be a term including some of the arguments; the rest of the arguments of the call should be in the additional arguments of call/n.
  • call/n examples (hiord):

    Given:

    :- module(_,_).
    % Meta-calls: call/n
    % ------------------
    
    q(a,1).
    q(b,2).
    We first load hiord into the top level:

    ?- use_package(hiord).
    Try:

    ?- P = q, call(P,X,Y).
    ?- P = q(a), call(P,Y).
    ?- P = append, call(P,[1,2],[3,4],Y).
    ?- P = append(X,Y), call(P,[1,2,3,4]).
  • There is also a higher-order library hiordlib:

    Given:

    plus(X,Y,Z) :- number(X),number(Y), Z is X + Y.
    plus(X,Y,Z) :- number(X),number(Z), Y is Z - X.
    plus(X,Y,Z) :- number(Y),number(Z), X is Z - Y.
    Try:

    ?- use_module(library(hiordlib)).
    ?- maplist(plus, [1,2,3], [4,5,6], R).
    ?- maplist(plus, [1,2,3], L2, [5,7,9]).
    ?- maplist(append, [ [a,b], [1,2] ], [ [c,d], [3,4] ], Z).
    ?- maplist(append, [ [a,b], [1,2] ], Y, [[a,b,c,d],[1,2,3,4]] ).
    ?- maplist(append, [ [a],   X,     X ], [ [c,d], [e,f], Y ], [ X,     Y,     Z] ).

Further Higher Order Facilities (Ciao)

  • In Ciao the hiord package allows writing G(Y,Z) and provides much additional functionality, such as anonymous predicates (see more in the hiordlib library).
  • Examples:

    :- module(_,_).
    % Some additional functionality via the hiord package
    % ---------------------------------------------------
    
    q(a,1).
    q(b,2).
    • A) Optional special syntax for call/n:

      Load hiord package in top level:

      ?- use_package(hiord).
      Try (ask for more solutions):

      ?- P = q, P(X,Y).
      ?- P = append, P([1,2],[3,4],Y).
      ?- P = append(X,Y), P([1,2,3,4]).
    • B) Anonymous predicates (read _ as lambda):

      ?- P = { _(X,Y) :- Y is X+1 }, P(3,R).
    • And much more!

Meta-calls – Aggregation Predicates

  • Other meta-calls are, e.g., findall/3, bagof/3, and setof/3.
  • findall(Term, Goal, ListResults): ListResults is the set of all instances of Term such that Goal is satisfied

    • If there are no instances of Term, ListResults is []
    • For termination, the number of solutions should be finite (and enumerable in finite time).
likes(bill,  cider).
likes(dick,  beer).
likes(tom,   beer).
likes(tom,   cider).
likes(harry, beer).
likes(jan,   cider).
?- findall(X, likes(X,Y), S).
S = [bill,dick,tom,tom,harry,jan] ? 
yes
?- findall(X, likes(X,water), S).
S = [] ? 
yes
?- 

Meta-calls – Aggregation Predicates (Contd.)

  • setof(Term, Goal, ListResults): ListResults is the ordered set (no duplicates) of all instances of Term such that Goal is satisfied

    • If there are no instances of Term the predicate fails
    • The set should be finite (and enumerable in finite time)
    • If there are un-instantiated variables in Goal which do not also appear in Term then a call to this built-in predicate may backtrack, generating alternative values for ListResults corresponding to different instantiations of the free variables of Goal
    • Variables in Goal will not be treated as free if they are explicitly bound within Goal by an existential quantifier as in Y^...
      (then, they behave as in findall/3)
  • bagof/3:
    same, but returns list unsorted and with duplicates (in backtracking order).

Meta-calls – Aggregation Predicates: Examples and Exercises

Meta-calls: aggregation predicates
Examples of findall/3, setof/3, and bagof/3.

  • Some simple examples with member/2:

    ?- findall(X, member(X,[d,a,b,c,b,d]), L).
    ?- setof(X, member(X,[d,a,b,c,b,d]), L).
    ?- bagof(X, member(X,[d,a,b,c,b,d]), L).
    ?- findall(X, (member(X,[d,a,b,c,b,d]), X @< c), L).
  • Our sample database:

    likes(bill,  cider).
    likes(dick,  beer).
    likes(tom,   beer).
    likes(tom,   cider).
    likes(harry, beer).
    likes(jan,   cider).
  • Some findall/3 examples:

    Try (asking for more solutions):

    ?- findall(X, likes(X,Y), S).
    ?- findall(X, likes(X,water),S).
    Note the empty list.
  • Some setof/3 examples:

    ?- setof(X, likes(X,Y), S).
    ?- setof(X, Y^likes(X,Y), S).
    ?- setof(X, likes(X,water), S).
    Note that it fails.
  • Some bagof/3 examples:

    ?- bagof(X, likes(X,Y), S).
    ?- bagof(X, Y^likes(X,Y), S).
    ?- bagof(X, likes(X,water),S).
    Note that it fails.
  • Examples using a term in the first argument:

    ?- findall( Y-L ,  setof(X,likes(X,Y),L),  Pairs).
    ?- findall(beer_lover(X), likes(X,beer), S).
    ?- setof((Y,S), setof(X, likes(X,Y), S), SS).
  • Another setof/3 example:

    subset([],[]).
    subset([X|Xs],[X|Ys]) :-
        subset(Xs,Ys).
    subset([_|Xs],Ys) :-
        subset(Xs,Ys).
    
    powerset(Set,Pset) :-
        setof(X,subset(Set,X),Pset).
    Try (asking for more solutions):

    ?- subset([1,2,3],S).
    ?- powerset([1,2,3],Pset).

Meta-calls – Negation as Failure

  • Uses the meta-call facilities, the cut and a system predicate fail that fails when executed (similar to calling a=b).

    :- module(_,_).
    % Meta-calls and cut: Negation as failure
    % ---------------------------------------
    
    not( Goal) :- call(Goal), !, fail.
    not(_Goal).
    • This is available from the library as \+/1.

      Try:

      ?- not( member(c, [a, k, l]) ).
      ?- \+ member(c, [a, k, l]).
    • Another example:

      unmarried_student(X) :-
          \+ married(X),
          student(X).
      
      student(joe).
      married(john).
      Note that Joe is an unmarried student, but John is NOT an unmarried student!

      Try:

      ?- unmarried_student(joe).
      ?- unmarried_student(john).
      ?- unmarried_student(X).
      This last one is wrong, an unmarried student does exist!
    • Observation: not/1 is correct if the argument is ground (is a term that does not contain variables). ground/1 is available:

      Try:

      ?- ground(unmarried_student(joe)).
      ?- ground(unmarried_student(X)).
    • Another example: overlap/2 and disjoint/2:

      overlap(S1,S2) :-  % S1 and S2 overlap if they share an element
          member(X,S1), member(X,S2).
      
      disjoint(S1,S2) :- \+ overlap(S1,S2).
      Try:

      ?- disjoint([1,2,3,4],[a,b,c]).
      ?- disjoint([1,2,3,4],[2,4,c]).
  • It will never instantiate the variables of the goal.

    • Using \+ twice useful to test without binding variables. E.g., \+ \+ X = 1, checks if X is bound (or can be bound) to 1, without binding X if it is free.
  • Termination of \+ Goal depends on termination of Goal. \+ Goal will terminate if a success node for Goal is found before an infinite branch.
  • A frequent fix is put the generator first (i.e., so that that X is ground when we get to the \+) – check it in the exercise... exercise! ▶

Meta-calls – Negation as Failure – Ensuring Correctness

  • Recall the unmarried_student/1 example:

    :- module(_,_).
    % Meta-calls and cut: Negation as failure -- ensuring correctness
    % ---------------------------------------------------------------
    
    unmarried_student(X) :-
        \+ married(X),
        student(X).
    
    student(joe).
    married(john).
    Try:

    ?- unmarried_student(X).
    Which we know is wrong, since an unmarried student does exist.
  • We can check that negation is called with a ground term (a better, safer implementation of negation):

    not_(G) :-
        ground(G), !,
        \+ G.
    not_(G) :-
        write('ERROR: Non-ground goal in negation: '), write(G), nl,
        abort.
    
    unmarried_student_with_check(X) :-
        not_(married(X)),
        student(X).
    Try:

    ?- unmarried_student_with_check(joe).
    ?- unmarried_student_with_check(john).
    ?- unmarried_student_with_check(X).
  • Or using assertions. We load the assertions package and run-time checking of assertions:

    :- use_package([assertions,modes,rtchecks]).
  • We define not__/1 which simply calls \+ but protected by an assertion which requires the goal in G to be ground on calls (: field):

    :- pred not__(G) : ground(G).
    not__(G) :- \+ G.
    And we use this not__/1 in the example:

    assrt_unmarried_student(X) :-
        not__( married(X) ),
        student(X).
    This will be checked, in this case dynamically (by rtchecks):

    ?- assrt_unmarried_student(joe).
    ?- assrt_unmarried_student(X).
  • Another example of meta-call + cut: predicate once/1 for getting only one solution from a goal. (once(G): only one solution of G):

    once(X) :- call(X), !.
    Try (asking for more alternatives):

    ?- member(X,[1,2,3]).
    ?- once(member(X,[1,2,3])).

Cut-Fail

  • Cut-fail combinations allow forcing the failure of a predicate — somehow specifying a negative answer (useful but dangerous!).
  • Example: testing groundness. A term is ground if it contains no variables. We use the cut-fail to fail as soon as a free variable is found:
    Fail can be easily programmed in pure logic:

    :- module(_,_).
    % Meta-calls and cut: cut-fail
    % ----------------------------
    
    myfail :- a=b.
    (It is available simply as fail).

    We tell the compiler we are redefining the ground/1 built-in:

    :- redefining(ground/1).
    Definition of ground/1 using cut and fail:

    ground(Term) :- 
        var(Term), 
        !, 
        fail.
    ground(Term) :- 
        nonvar(Term), 
        functor(Term,_F,N),
        ground_(N,Term).
    
    ground_(0,_T).       % All subterms traversed
    ground_(N,T) :- 
        N>0, 
        arg(N,T,Arg), 
        ground(Arg), 
        N1 is N-1, 
        ground_(N1,T).
    Try (also in the debugger):

    ?- ground(a).
    ?- ground(X).
    ?- ground(f(a,f(X,d),b)).
    Comment out the first clause of ground/1 and guess what happens.

    Try (also in the debugger):

    ?- ground(f(a,f(c,d),Z)).

Dynamic Program Modification (I)

  • assert/1, retract/1, abolish/1, ...
  • Very powerful: allow run–time modification of programs. Can also be used to simulate global variables.
  • Sometimes this is very useful, but very often a mistake:
    • Code hard to read, hard to understand, hard to debug.
    • Typically, slow.
  • Program modification has to be used scarcely, carefully, locally.
  • Still, assertion and retraction can be logically justified in some cases:
    • Assertion of clauses which logically follow from the program (lemmas).
    • Retraction of clauses which are logically redundant.
  • Other typically non-harmful use: simple global switches.
  • Behavior/requirements may differ between Prolog implementations.
    Typically, new predicates (not already defined in the program) can always be asserted; else they must be declared :- dynamic.

Dynamic Program Modification (II)

:- module(_,_).
Examples for dynamic predicates

  • A) New, dynamically created predicates are directly modifiable:

    Try (asking for more solutions):

    ?- p(X).
    ?- assert(p(a)).
    ?- p(X).
    ?- assert(p(b)).
    ?- p(X).
    ?- retract(p(a)).
    ?- retract(p(b)).
    ?- p(X).
    ?- abolish(p/1).
    ?- p(X).
  • B) If the predicate is already defined in the program:

    related(3,4).
    It must be declared dynamic for it to be modifiable:

    :-  dynamic related/2.
    We define some operations on related/2:

    relate_numbers(X,Y) :- assert(related(X,Y)).
    unrelate_numbers(X,Y) :- retract(related(X,Y)).
    Try:

    ?- related(3, 4).
    ?- related(1, 2).
    ?- relate_numbers(1, 2).
    ?- related(1, 2).
    ?- related(X, Y).
    ?- unrelate_numbers(1, 2).
    ?- related(1, 2).
    ?- abolish(related/2).
    ?- related(1,2).
  • C) Rules can also be asserted dynamically: a definition of append that we can add or delete dynamically:

    :- dynamic app/3.
    
    % Create the definition of append
    define_app :-
        assert(   app([], X, X) ),
        assert( ( app([X|Y], Z, [X|W]) :- app(Y,Z,W) ) ).
    
    % Erase the definition of append
    erase_app :- 
        abolish(app/3).
    Try:

    ?- app([1,2],[3,4],Y).
    ?- erase_app.
    ?- app([1,2],[3,4],Y).
    ?- define_app.
    ?- app([1,2],[3,4],Y).
    ?- app(X,Y,[1,2,3,4]).
    ?- erase_app.
    ?- app(X,Y,[1,2,3,4]).

Dynamic Program Modification (III)

  • Example: computing the Fibonacci sequence

    The Fibonacci sequence: 0, 1, and then: element n is element n-1 + element n-2,

    i.e., 0 1 1 2 3 5 8 13 21 ...

    • 1a) First version, applying directly the definition:

      :- module(_,_).
      % Dynamic program modification example: using lemmas
      % --------------------------------------------------
      
      fib(0, 0). 
      fib(1, 1). 
      fib(N, F) :-  
          N > 1,        
          N1 is N - 1, 
          N2 is N - 2, 
          fib(N1, F1), 
          fib(N2, F2),  
          F is F1 + F2. 
    • 1b) Same, in functional notation:

      :- use_package(fsyntax).
      :- fun_eval arith(true).
      
      ffib(0) := 0.
      ffib(1) := 1.
      ffib(N) := ~ffib(N-1) + ~ffib(N-2) :- N>1.
    • 2) A version that records 'lemmas' (things already proved):

      First, we define a table (with a dynamic predicate) that holds the first two elements, i.e., for N=0 and N=1:

      :- dynamic lemma_fib/2.
      lemma_fib(0, 0). 
      lemma_fib(1, 1). 
      To compute the rest:

      lfib(N, F) :-
          % First check if we have the Nth element in the table, 
          % and we take it from there. 
          lemma_fib(N, F),
          !. 
      lfib(N, F) :- 
          % Else, we compute the Nth number...
          N > 1,   
          N1 is N - 1,  
          N2 is N - 2, 
          lfib(N1, F1), 
          lfib(N2, F2), 
          F is F1 + F2,     
          % ...and once we have it we record it: 
          assert(lemma_fib(N, F)). 
      Try (compare the computation time, adjust the values for your CPU):

      ?- fib(30,Y).
      ?- fib(31,Y).
      ?- fib(32,Y).
      ?- fib(35,Y).
      ?- lfib(30,Y).
      ?- lfib(35,Y).
      ?- lfib(200,Y).
      ?- lfib(1000,Y).
  • Do these exercises for a more complete comparison: exercise! ▶
"Those who cannot remember the past are condemned to repeat it"

Meta-Interpreters

  • clause(<head>,<body>)
    • Reads a clause head:-body from the program.
    • For facts body is true.
  • To be visible to clause/2 a predicate must be declared dynamic.
  • Simple ("vanilla") meta-interpreter:

    We use this (Ciao-specific) version of the dynamic package that makes dynamic clauses visible to clause/2:

    :- module(_,_,[]).
    % Meta-interpreters
    % -----------------
    
    :- use_package(dynamic_clauses).
    • Declare lappend/3 dynamic, and thus visible to clause/2:

      :- dynamic lappend/3.
      
      lappend([],X,X).
      lappend([X|Y],Z,[X|W]) :- lappend(Y,Z,W).
    • clause/2 reads the clauses of (dynamic) predicates:

      Try (ask for several solutions):

      ?- clause(lappend(A,B,C),Body).
    • The 'vanilla' 3-line meta-interpreter:

      solve( true  ) :- !.
      solve( (A,B) ) :- !, solve(A),    solve(B).
      solve( A     ) :- clause(A,B), solve(B).
      Try (asking for multiple solutions):

      ?- solve(lappend([1,2],[3,4],L)).
      ?- solve(lappend(X,Y,[1,2,3,4])).
    • This meta-interpreter also implements backtracking! Note that clause/2 introduces choice-points since A can unify with several clause heads.
  • Interactions with module system: remember that clauses must be dynamic (and use the dynamic_clauses package).

Meta-Interpreters: extending the basic meta-interpreter

  • The basic meta-interpreter code:

    solve( true  ) :- !.
    solve( (A,B) ) :- !, solve(A), solve(B).
    solve( A     ) :- clause(A,B), solve(B).
    can be easily extended to do many tasks: tracing, debugging, explanations in expert systems, implementing other computation rules, ...
  • E.g., an interpreter that counts the number of (forward) steps:

    Declare lappend/3 dynamic, and thus visible to clause/2:

    :- module(_,_,[]).
    % Meta-interpreters: extending the basic meta-interpreter
    % -------------------------------------------------------
    
    % Ciao-specific: version of 'dynamic' package that makes dynamic
    % clauses visible to `clause/2`
    :- use_package(dynamic_clauses).
    
    :- dynamic lappend/3.
    
    lappend([],X,X).
    lappend([X|Y],Z,[X|W]) :- lappend(Y,Z,W).
    A version of the meta-interpreter that uses an extra argument to count the number of (forward) steps:

    csolve( true,  0) :- !.
    csolve( (A,B), N) :- !, csolve(A,NA), csolve(B,NB), N is NA+NB.
    csolve( A,     N) :- clause(A,B),  csolve(B,N1), N is N1+1.
    Try:

    ?- csolve(lappend([1,2],[3,4],L),N).
    ?- csolve(lappend([1,2,a,b],[3,4],L),N).
    ?- csolve(lappend([1,2],[3,4,a,b],L),N).
    What does the cost (number of steps) of a call to lappend/3 depend on?

  • Do this exercise to develop other meta-interpreter variations: exercise! ▶

Incomplete Data Structures - Example: Difference Lists

  • Consider [1,2,3]: it is a closed list; cannot simply add at the end.
  • Idea: provide direct access to list tail to be able to append in constant time:
    • Put a variable at the end (T) instead of []: [1,2,3|T].
    • Pass T along with the list in a pair ( L-T) to give direct access to the tail.
  • The idea expressed as a (pseudo-)type:

    dlist(X-X).
    dlist([_|DL]-Y) :- dlist(DL-Y).
    (Note that we could also need var/1 and ==/2 checks.)

  • Allows us to keep a pointer to the end of the list → append in constant time:

    append_dl(B1-E1,B2-E2,B3-E3) :- B3=B1, E3=E2, B2=E1.
    Or, more compactly: append_dl(X-Y,Y-Z,X-Z).
    Actually, no call to append_dl is normally necessary: done "in place."
  • ... But can only be done once (see later).
One can also build difference (open ended) trees, dictionaries, queues, etc., by leaving variables at the ends (e.g., at the leaves for trees).

Playing with difference lists

Some queries with difference lists

  • We refer to them as 'incomplete' lists because we can easily access the end T and add by instantiating T:

    ?- L = [1,2,3|T]-T,        L = _-T, T = [4,5].
  • Expressing the idea as a (pseudo-)type:

    dlist(X-X). 
    dlist([_|DL]-X) :- dlist(DL-X).
    (note that we could also need var/1 and ==/2 checks.)

    Try (ask for multiple solutions):

    ?- dlist(L).
  • Creating two difference lists L1 and L2 and appending them (in constant time!):

    ?- L1 = [1,2,3|X], L2 = [4,5|Y], L2=X.
    But note that we have modified the tail of the first list: we cannot append to it again.
  • A predicate for appending difference lists:

    append_dl(B1-E1,E1-E2,B1-E2).
    Try:

    ?- append_dl( [1,2,3|X]-X, [4,5|Y]-Y, L).
    L has the resulting (appended) difference list.

    But note that we have modified the first list: we cannot append to it again.

    ?- append_dl(L-X,[4,5|Y]-Y,[1,2,3,4,5|Z]-Z).
  • Other uses:

    Checking:

    ?- append_dl( [1,2,3|X]-X, [4,5|Y]-Y, [1,2,3,4,5|Z]-Z).
    Subtracting:

    ?- append_dl( L-X, [4,5|Y]-Y, [1,2,3,4,5|Z]-Z).
    ?- append_dl( L1-X, L2-Y, [1,2,3,4,5|Z]-Z).
    But only one solution, and we cannot append again.
  • Actually, append_dl is normally never used: done “in place” as above (and see, e.g., the qsort/2 example).

Quick-sort using standard lists and append

  • qsort(L,S): S is the list L, sorted (using quick-sort).

    :- module(_,_).
    
    qsort([],[]).
    qsort([X|L],S) :-         % Take first element of list in X
       partition(L,X,LS,LB),  % LS are the elements of L < X
                              % LB are the elements of L >= X
       qsort(LS,LSS),         % LSS is LS sorted 
       qsort(LB,LBS),         % LBS is LB sorted 
       append(LSS,[X|LBS],S). % We append the small ones sorted to 
                              % the big ones sorted (w/X in front)
    
    partition([],_P,[],[]).
    partition([E|R],P,[E|Smalls],Bigs) :- % Take first element E
        E < P,            % If E < P add to list of smaller ones
        partition(R,P,Smalls,Bigs).
    partition([E|R],P,Smalls,[E|Bigs]) :-
        E >= P,           % If E >= P add to list of larger ones
        partition(R,P,Smalls,Bigs).
Try:

?- qsort([5,2,1,3,7,6], SL).

Quick-sort using standard lists and append

  • qsort(L,S): S is the list L, sorted (using quick-sort).

    :- module(_,_).
    
    qsort([],[]).
    qsort([X|L],S) :-            % X=5, L=[2,1,3,7,6]
       partition(L,X,LS,LB),     % LS=[2,1,3]
                                 % LB=[7,6]
       qsort(LS,LSS),            % LSS=[1,2,3]
       qsort(LB,LBS),            % LBS=[6,7]
       append(LSS,[X|LBS],S).    % call: append([1,2,3],[5,6,7],S)
                                 % S=[1,2,3,5,6,7]
    
    partition([],_P,[],[]).
    partition([E|R],P,[E|Smalls],Bigs) :- 
        E < P,
        partition(R,P,Smalls,Bigs).
    partition([E|R],P,Smalls,[E|Bigs]) :-
        E >= P,
        partition(R,P,Smalls,Bigs).
Try:

?- qsort([5,2,1,3,7,6], SL).

Quick-sort using difference lists (no append!)

  • First list L is a normal list, second is built as a difference list (SL-SLE).
  • Version 1: using -/2 functor and explicit unifications (for explanation):

    :- module(_,_).
    
    qsort_dl(L,SL) :-           % L  = [5,2,1,3,7,6]
        qsort_dl_(L,SL-SLE),    % SL = [1,2,3,5,6,7|SLE]
        SLE = [].               % SL = [1,2,3,5,6,7]
    
    qsort_dl_([],SLE-SLE).
    qsort_dl_([X|L],SL-SLE) :-  % X = 5, L = [2,1,3,7,6]
        partition(L,X,S,B),     % S = [2,1,3], B = [7,6]
        qsort_dl_(S,SS-SSE),    % SS  = [1,2,3|SSE]
        qsort_dl_(B,BS-BSE),    % BS  = [6,7|BSE]
        SSE = [X|BS],           % SSE = [5,6,7|BSE]
        SL = SS,                % SL  = [1,2,3,5,6,7|BSE]
        SLE = BSE.              % SL  = [1,2,3,5,6,7|SLE]
    ?- qsort_dl([5,2,1,3,7,6], SL).
    Run it in the debugger! (Partition is same as in std. qsort:)

    partition([],_P,[],[]).
    partition([E|R],P,[E|Smalls],Bigs) :- % Take first element E
        E < P,            % If E < P add to list of smaller ones
        partition(R,P,Smalls,Bigs).
    partition([E|R],P,Smalls,[E|Bigs]) :-
        E >= P,           % If E >= P add to list of larger ones
        partition(R,P,Smalls,Bigs).

Quick-sort using difference lists (no append!)

  • First list L is a normal list, second is built as a difference list (SL-SLE).
  • Version 2: still using -/2 functor, but with unifications inlined:

    :- module(_,_).
    
    qsort_dl(L,SL) :-
        qsort_dl_(L,SL-[]).
    
    %    
    qsort_dl_([],SLE-SLE).
    qsort_dl_([X|L],SL-SLE) :-
        partition(L,X,S,B),
        qsort_dl_(S,SL-[X|BS]),
        qsort_dl_(B,BS-SLE).
    ?- qsort_dl([5,2,1,3,7,6], SL).
    Run it in the debugger! (Partition is same as in std. qsort:)

    partition([],_P,[],[]).
    partition([E|R],P,[E|Smalls],Bigs) :- E <  P, partition(R,P,Smalls,Bigs).
    partition([E|R],P,Smalls,[E|Bigs]) :- E >= P, partition(R,P,Smalls,Bigs).

Quick-sort using difference lists (no append!)

  • First list L is a normal list, second is built as a difference list (SL-SLE).
  • Version 3: using extra arguments, unifications inlined:

    :- module(_,_).
    
    qsort_dl(L,SL) :-
        qsort_dl_(L,SL,[]).
    
    %
    qsort_dl_([],SLE,SLE).
    qsort_dl_([X|L],SL,SLE) :-
        partition(L,X,S,B),
        qsort_dl_(S,SL,[X|BS]),
        qsort_dl_(B,BS,SLE).
    ?- qsort_dl([5,2,1,3,7,6], SL).
    Run it in the debugger! (Partition is same as in std. qsort:)

    partition([],_P,[],[]).
    partition([E|R],P,[E|Smalls],Bigs) :- E <  P, partition(R,P,Smalls,Bigs).
    partition([E|R],P,Smalls,[E|Bigs]) :- E >= P, partition(R,P,Smalls,Bigs).
Do some difference list exercises: exercise! ▶

Parsing (using append and traditional lists)

?- myphrase([t,h,e,' ',p,l,a,n,e,' ',f,l,i,e,s]).

  • A first approach at parsing: using standard lists and append to match:

    :- module(_,_).
    % :- use_package(sr/bfall).
    
    myphrase(P) :-
        % An article (A) must be at beginning of P, T1 is the rest:
        append_(A,T1,P),    article(A), 
        append_(S1,T2,T1),  spaces(S1), % Spaces must follow
        append_(N,T3,T2),   noun(N),    % A noun must follow
        append_(S2,V,T3),   spaces(S2), % Spaces must follow
                            verb(V).    % Must end with a verb
    
    article([a]).
    article([t,h,e]).
    
    spaces([' ']). 
    spaces([' ' | T]) :- 
        spaces(T).
    
    noun([c,a,r]).
    noun([p,l,a,n,e]).
    
    verb([f,l,i,e,s]).
    verb([d,r,i,v,e,s]).
    
    % - Local version for alternate search rule
    append_([],X,X).
    append_([X|Y],Z,[X|W]) :- append_(Y,Z,W).
  • Try:

    ?- myphrase([t,h,e,' ',p,l,a,n,e,' ',f,l,i,e,s]).
    ?- myphrase([t,h,e,' ',p,l,a,n,e]).
  • And we can generate:

    ?- myphrase(X).
  • Try also uncommenting the :- use_package(sr/bfall). above and, again:

    ?- myphrase(X).
    We get fair generation!

Parsing (using difference lists)

?- myphrase([t,h,e,' ',p,l,a,n,e,' ',f,l,i,e,s],[]).

  • A second approach: using difference lists - recall that we can add at the end L = [t,h,e|T], T = [' ',p,l,a,n,e|T2], ... by instantiating T, then T2, etc.

    :- module(_,_).
    % :- use_package(sr/bfall).
    
    myphrase(P,C) :-
        % Article is beginning of phrase, rest is beginning of spaces
        article(P,CA), spaces(CA,CS1),
        noun(CS1,CN),  spaces(CN,CS2),
        verb(CS2,C).
    
    article([a|T],T).
    article([t,h,e|T],T).
    
    spaces([' ' | T],T).
    spaces([' ' | Y],T) :- 
        spaces(Y,T).
    
    noun([c,a,r | T],T).
    noun([p,l,a,n,e | T],T).
    
    verb([f,l,i,e,s | T],T).
    verb([d,r,i,v,e,s | T],T).
    ?- myphrase([t,h,e,' ',p,l,a,n,e,' ',f,l,i,e,s|T], T).
    ?- myphrase([t,h,e,' ',p,l,a,n,e|T], T).
  • We can also call with a closed list (better behavior in case of failure):

    ?- myphrase([t,h,e,' ',p,l,a,n,e,' ',f,l,i,e,s], []).
    ?- myphrase([t,h,e,' ',p,l,a,n,e], []).
  • And we can generate both:

    ?- myphrase(X, T).
    ?- myphrase(X, []).
  • Try also uncommenting the :- use_package(sr/bfall). above and, again:

    ?- myphrase(X, []).

Parsing (same, using string syntax)

?- myphrase("the plane flies",[]).

  • Using strings – reminder: strings "..." are lists of characters i.e., X = "the" means X = [0't,0'h,0'e], i.e., X = [116,104,101].
  • myphrase/2 is the same as before:

    :- module(_,_).
    % :- use_package(sr/bfall).
    
    myphrase(P,C) :-
        article(P,CA), spaces(CA,CS1),
        noun(CS1,CN),  spaces(CN,CS2),
        verb(CS2,C).
  • Reminder: || is for string like | for lists, i.e., what follows is the tail of the list:

    article( "a"   || T, T).
    % Translates to  article( [ 97|T], T).
    % i.e.,          article( [0'a|T], T).
    article( "the" || T, T).
    % Translates to  article( [0't,0'h,0'e|T], T).
    
    spaces( " "    || T, T).
    spaces( " "    || Y, T) :- spaces(Y, T).
    
    noun( "car"    || T, T).
    noun( "plane"  || T, T).
    
    verb( "flies"  || T, T).
    verb( "drives" || T, T).
    ?- myphrase("the plane flies", []).
    ?- myphrase("the plane", []).
    ?- myphrase(S, []).
    ?- set_prolog_flag(write_strings, on).
    ?- myphrase(S, []).
  • Try also uncommenting the :- use_package(sr/bfall). above and, again:

    ?- myphrase(S, []).

Parsing (same, using additional syntax: DCGs)

  • We can add a syntactic transformation to avoid writing all the auxiliary variables.
  • → The result is called a Definite Clause Grammar ("DCG").
  • Using a DCG:

    :- module(_,_).
    :- use_package(dcg).
    % :- use_package(sr/bfall).
    
    myphrase --> article, spaces, noun, spaces, verb.
    
    article --> "a".
    article --> "the".
    
    spaces --> " ".
    spaces --> " ", spaces.
    
    noun --> "car".
    noun --> "plane".
    
    verb --> "flies".
    verb --> "drives".
    ?- set_prolog_flag(write_strings, on).
    ?- myphrase("the plane flies", []).
  • Or, using the phrase/2 built-in:

    ?- phrase(myphrase,"the plane flies").
    ?- myphrase("the plane", []).
    ?- myphrase(S, []).
  • Try also uncommenting the :- use_package(sr/bfall). above and, again:

    ?- myphrase(S,[]).

Parsing (same, using additional syntax: DCGs)

  • We can add a syntactic transformation to avoid writing all the auxiliary variables.
  • → The result is called a Definite Clause Grammar ("DCG").
  • Using a DCG:

    :- module(_,_).
    :- use_package(dcg).
    % :- use_package(sr/bfall).
    
    myphrase --> article, spaces, noun, spaces, verb.
    % Translates to:
    % myphrase(X,C) :- article(X,CA), spaces(CA,CS1), noun(CS1,CN),
    %                  spaces(CN,CS2), verb(CS2,C).
    
    article --> "a".  % Translates to (*): article( "a"   || X, X).
    article --> "the".
    
    spaces --> " ".
    spaces --> " ", spaces.
    
    noun --> "car".
    noun --> "plane".
    
    verb --> "flies".
    verb --> "drives".
    ?- set_prolog_flag(write_strings, on).
    ?- myphrase("the plane flies", []).
  • Or, using the phrase/2 built-in:

    ?- phrase(myphrase,"the plane flies").
    ?- myphrase("the plane", []).
    ?- myphrase(S, []).
  • Try also uncommenting the :- use_package(sr/bfall). above and, again:

    ?- myphrase(S,[]).
  • (*) In fact, article --> "a". translates to article(X,Y) :- 'C'(X,97,Y)., and article --> "the". to article(X,Y) :- 'C'(X,116,Y1), 'C'(Y1,104,Y2), 'C'(Y2,101,Y)., where 'C'(L,X,T) is a built-in that means L = [X|T].

Parsing + actions (calling plain Prolog from DCGs)

  • We can intersperse additional actions within the grammar:
    • Plain Prolog can be called between { ... }.
    • Additional arguments can be added; they go before the implicit ones.
  • Example: we add an additional argument to count the characters parsed:

    :- module(_,_).
    :- use_package(dcg).
    % :- use_package(sr/bfall).
    
    myphrase(N) --> article(AC), spaces(S1), noun(NC), spaces(S2), 
                      verb(VC), { N is AC + S1 + NC + S2 + VC }.
    % Translates to: 
    % myphrase(N,X1,X6) :- article(AC,X1,X2), spaces(S1,X2,X3), noun(NC,X3,X4),
    %                      spaces(S2,X4,X5), verb(VC,X5,X6), N is AC+S1+NC+S2+VC.
    
    article(1) --> "a".
    article(3) --> "the".
    
    spaces(1) --> " ".
    spaces(N) --> " ", spaces(N1), { N is N1+1 }.
    
    noun(3) --> "car".
    noun(5) --> "plane".
    
    verb(5) --> "flies".
    verb(6) --> "drives".
Try:

?- set_prolog_flag(write_strings,on).
?- myphrase(NChars, "the plane flies", []).
Or, using the phrase/2 built-in:

?- phrase(myphrase(NChars), "the plane flies").
?- myphrase(NChars, "the plane", []).
?- myphrase(NChars, S, []).
Try also uncommenting the :- use_package(sr/bfall). above, reloading, and, again:

?- myphrase(NChars,S,[]).

Do some exercises on grammars: exercise! ▶

Other issues in Prolog (see "The Art of Prolog" and Bibliography)

  • Repeat loops and "failure-driven" loops:

    main(_) :- repeat, read(X), process(X).
    
    process(end).
    process(X) :- display(X), nl, fail.
  • Exception handling.
  • Extending the syntax beyond operators: term expansions/macros →packages.
  • Delay declarations/concurrency.
  • Operating system interface (and sockets, etc.).
  • Foreign language interfaces (e.g., C).
  • Many other built-ins...
  • ...

Some Typical Libraries in Prolog Systems

  • Most systems have a good set of libraries.
  • Worth checking before re-implementing existing functionality!
  • Some examples:

ArraysAssocAttributesHeaps
ListsTerm UtilitiesOrdsetQueues
RandomSystem UtilitiesTreeUGraphs
WGraphsSocketsLinda/DistributionPersistent DB
CLPBCLPQRCLPFDObjects
GCLATclTkTracingChars I/O
Runtime UtilitiesTimeoutXrefsWWW
Java Interface.........

Some Additional Libraries and Extensions (Ciao)

Other systems may offer additional extensions. Some examples from Ciao:

  • Other execution rules:

    • Breadth-first, Iterative-deepening, Random, ...
    • Tabling
    • CASP (negation with multiple models)
    • Andorra ("determinate-first") execution, fuzzy Prolog, ...
  • Interfaces to other languages and systems:

    • Interfaces to C, Java, JavaScript, Python, LLVM, ...
    • SQL database interface and persistent predicates
    • Web/HTML/XML/CGI programming (PiLLoW) / HTTP connectivity / JSON / compilation to JavaScript ...
    • Interfaces to solvers (PPL, Mathematica, MiniSAT, Z3, Yices, ...)
    • Graphviz, daVinci interfaces
    • Interfaces to Electron, wxWidgets, Tcl/Tk, VRML (ProVRML), ...
    • Calling Emacs from Prolog, etc.

Some Additional Libraries and Extensions (Ciao, Contd.)

  • Many syntactic and semantic extensions:
    • Functional notation
    • Higher-order
    • Terms with named arguments - records/feature terms
    • Multiple argument indexing
    • The script interpreter
    • Active modules (high-level distributed execution)
    • Concurrency/multithreading
    • Attributed variables
    • Object oriented programming
    • ...

Some Additional Libraries and Extensions (Ciao, Contd.)

  • Constraint programming (CLP)
    • rationals, reals, finite domains, ...
    • CHR (constraint handling rules), Gecode, ...
  • Assertions:
    • Regular types, Modes, Determinacy, etc.
    • Other properties
    • Run-time checking of assertions
    • Assertion-based unit tests and automatic test case generation
    • Compile-time property inference and assertion checking (CiaoPP).
  • Additional programming support:
    • Automatic documentation (LPdoc).
    • Partial evaluation, optimization, parallelization (CiaoPP).
    • ...