☰

Additional Features of Prolog
and the ISO Standard

Course Notes (lecture version)


These are the notes accompanying the slides Additional Features of Prolog and the ISO Standard (3_prolog_language.pdf) of the Computational Logic course.

This is the lecture version: it follows the slides in order, but keeps much more of what was actually said in class — the asides, the reasons, the second explanations, and the things that were only ever said out loud. Where the slide gives a bullet, the lecture usually gave a paragraph and an example.

How to use this interactive document:

The code boxes, marked by a question mark (?) in the right top corner, allow interaction. Feel free to edit any program and make all the queries you want!

  • Code needs to be loaded before running, by pressing on the question mark — code box should be checked ✔.
  • Under each program there are editable boxes with a right-pointing triangle (▶) that allow interaction with the code (once loaded!) by issuing queries.
  • Expected answers are printed in plain text underneath each query box, so you can read the notes straight through without running anything.
  • When available, pressing the northeast pointing arrow (↗) will load the code in a separate Prolog playground window.

What Prolog adds to pure logic programming

Slide 2 — The ISO Prolog standard

Up to now we have been doing pure logic programming. It was not really a concrete language; it was the concept. We ran depth-first or breadth-first as we pleased, our data structures were all terms, and even the numbers were terms.

Prolog is a real, practical programming language based on that paradigm — the most popular one, and standardized by ISO. What does it add?

A fixed control. By default the search rule is depth-first and the computation rule is left-to-right. You no longer choose; the language has chosen for you. That is a loss of generality, and it buys a great deal: a program with a fixed, known execution order can be compiled into fast code — including native code — and then it runs at roughly the speed of any other language.

A large library of built-in predicates. Some declarative, some not — but all of them efficient. Arithmetic is the obvious case: built-in integer arithmetic is enormously faster than the Peano arithmetic we wrote in the previous part.

Things that go beyond first-order logic. Higher-order predicates; meta-logical predicates, which can ask whether something is still a variable — a question you cannot even state in pure logic; and modification of the program itself while it runs, programs that rewrite themselves as they go.

How much of a change is this, really? Less than it sounds, for most programs.

A program without search — one clause per procedure, or several clauses of which only one ever matches a given call, so the clauses work like a case statement rather than as alternatives — runs, under the left-to-right rule, exactly like its imperative or strict functional counterpart. Which means any imperative or functional program can be transliterated into Prolog and will behave the same way. The one adjustment is that you cannot overwrite a variable; but you just use a fresh one, and real systems offer imperative assignment anyway if you insist.

A program with search — at least one predicate where more than one clause matches — has no direct counterpart in imperative or functional programming. (List comprehensions are the nearest thing.)

And which of the two you have depends on the call mode. append/3 given two lists behaves exactly like an imperative concat; given the third list it becomes a search. Same program.

Drawbacks — and what modern systems do about them. A lot of people will tell you that Prolog has problems, and what they are usually describing is the Prolog of the 1970s. That is why the slide says "classical" systems. There are two classic complaints, and both are addressed today:

  • "Depth-first search is efficient but incomplete." True — but modern systems offer alternative search strategies: Ciao's bfall, tabling, iterative deepening, and others. You met sr/bfall already.
  • "There is no occurs check in unification, which led to unsoundness in older systems." Modern systems support regular (i.e. infinite) trees, so X = f(X) is a legitimate answer rather than a bug — which is, if you look at it closely, already constraint logic programming.

Slide 3 — An overview of the standard

The standard covers syntax (including operators) and the operational semantics; arithmetic; checking basic types and state; structure inspection and term comparison; input/output; the pruning operator (cut); meta-calls, higher order and aggregation; negation as failure and cut-fail; dynamic program modification; meta-interpreters; incomplete data structures; and exception handling. Definite clause grammars (DCGs), for parsing, were added more recently.

That list is also the plan of this part of the course, and of these notes.

If we stopped at the previous slide we would already have most of the difference between Prolog and pure logic programming — at least for pure Prolog programs, where the differences are that it runs depth-first and that this lets you control the search. Everything below is what the ISO standard adds on top. Other systems have plenty more, but those extensions are system-specific; the standard is what everyone agrees on.

Slide 4 — The programming interface

One thing the standard deliberately does not cover: how you actually write and run programs. The top level, the GUI, the interpreter, the compiler, the debugger, the module system — all of that is left to each implementation.

For the system used in this course, see the separate part on Developing Programs with a Logic Programming System.

Syntax and operators

Slide 5 — Prolog syntax and terminology

The syntax is essentially what we have been using. Variables start with a capital letter or an underscore; constants start with a lower-case letter or are written in single quotes — and if you want a quote inside the name you simply double it: 'Don''t'.

A terminology warning that catches everyone: in Prolog jargon, constants are called atoms. This has nothing to do with atoms in the logical sense (atomic formulas). When a Prolog manual says "atom" it means a constant symbol.

There are some new data types.

Numbers — real numbers now, not Peano numbers: 0, 999, -77, 5.23, 0.23e-5. And a very nice property of many current systems, Ciao among them: integers have infinite precision. There is no word size, and no overflow.

?- X is 1234567890123456789012345678901234567890 * 987654321.
Expected answer:

X = 1219326311248285321124828532112482853211126352690 ?
Strings are just lists of character codes. "foo" is the list of the ASCII codes of f, o and o:

?- X = "foo".
Expected answer:

X = [102,111,111] ?
Which is honest, but not very readable. There is a Prolog flag for it — flags are parameters of the system you can change at will — and this one is worth remembering:

?- set_prolog_flag(write_strings, on), X = "foo".
Expected answer:

X = "foo" ?
Now, when the system prints a list of numbers that all happen to be printable character codes, it prints it as a string. If the list contains something that is not, it gives up and prints the list.

Comments are % to the end of the line, or /* ... */ as in C.

Slide 6 — Defining operators

Certain functors and predicate symbols are predefined as infix, prefix or postfix operators, alongside the standard term notation — and you can add your own. This is not cosmetic: it is what makes programs and data files readable, and it is the mechanism behind language extensions.

The declaration is

:- op(<precedence>, <type>, <operator(s)>).
Precedence is an integer from 1 to 1200. The general rule: the operator with the highest precedence number is the principal functor. So if + has a higher precedence number than /, then a+b/c is a+(b/c), that is, +(a,/(b,c)). Note that some other languages number precedence the other way round. And parentheses always work: /(+(a,b),c) is (a+b)/c.

Type is one of nine mnemonics, and they read as pictures — f is the operator, x and y are the arguments:

  • infix: xfx (not associative), xfy (right associative), yfx (left associative)
  • prefix: fx (non-associative), fy (associative)
  • postfix: xf (non-associative), yf (associative)
An x where a y could be means you may not nest at this precedence; a y means you may. So xfx cannot be chained at all, xfy groups to the right, yfx to the left.

Operators is a single atom or a list of atoms.

write_canonical/1 settles every argument about precedence. It prints a term in canonical functor-and-arguments form, ignoring all operator declarations. When you are not sure how something parses, ask it:

?- write_canonical(1+2/4^2), nl.
Expected answer:

+(1,/(2,^(4,2)))
So 4^2 first, then 2 divided by that, then 1 plus all of it — which is what you wanted, but now you know.

Slide 7 — Operators in use

Here is an operator declaration doing real work. is_father_of is declared infix, non-associative, at precedence 500, and then the facts can be written the way you would say them out loud:

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

:- op(500, xfx, is_father_of).

john  is_father_of peter.
peter is_father_of jim.
?- is_father_of(X, Y).
Expected answer:

X = john,
Y = peter ?
The operator changes only how the term is written and read — internally it is still is_father_of(john,peter), as the query above shows by using the ordinary notation on a fact written in operator notation.

Operators are local to modules. This is a Ciao design decision, and an important one. Defining an operator inside a module does not make it visible at the top level or in modules that load it. So to use the infix form at the top level you must declare it there too:

?- op(500, xfx, is_father_of).

yes
?- X is_father_of Y.

X = john,
Y = peter ?
Why the locality? Because operators are how you define language extensions. One module can use functional syntax, another can use an imperative syntax, and neither disturbs the other. Without locality you could not compose them.

Which brings us to the point of the slide:

With this convention, Prolog clauses are also Prolog terms.

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)

Look at the last two lines. A clause is a term: the principal functor is :-, the neck; the first argument is the head; the second is the body, which for a conjunction is the term ','(q(Y),r(Z)) — nested to the right, exactly like the dots of a list.

This is what symbolic languages are supposed to have, and almost none do: a program is a data structure. Not a syntax tree you obtain by parsing, not an AST you build with a library — the program is data, and data is program, interchangeably. The program can read itself without anything called reflection or introspection.

That is why assert/1 — add a clause to the running program — will be a one-liner later on: you build a term and hand it over. Nothing has to be converted.

One syntactic trap. Parentheses must be used around operators whose priority is higher than 1000, the priority of ,. So you write ..., assert( (p :- q) ), ... — without the inner parentheses the comma-separated argument list wins and you get a syntax error.

Slide 8 — The standard operator table

These are the operators predefined in essentially every Prolog system, with their standard precedences:

:- op( 1200, xfx, [ :-, --> ]).
:- op( 1200,  fx, [ :-, ?- ]).
:- op( 1150,  fx, [ mode, public, dynamic,
                    multifile, block, meta_predicate,
                    parallel, sequential ]).
:- op( 1100, xfy, [ ; ]).
:- op( 1050, xfy, [ -> ]).
:- op( 1000, xfy, [ ',' ]).
:- op(  900,  fy, [ \+, spy, nospy ]).
:- op(  700, xfx, [ =, is, =.., ==, \==, @<, @>, @=<, @>=,
                                =:=, =\=, <, >, =<, >= ]).
:- op(  550, xfy, [ : ]).
:- op(  500, yfx, [ +, -, #, /\, \/ ]).
:- op(  500,  fx, [ +, - ]).
:- op(  400, yfx, [ *, /, //, <<, >> ]).
:- op(  300, xfx, [ mod ]).
:- op(  200, xfy, [ ^ ]).
Read it as a map. It tells you where you can slot a new operator in: above or below the clause neck, the comma, the disjunction, the implication, unification, the arithmetic comparisons, the arithmetic operators. In Ciao you can even unload them if you want a completely different language; they are loaded by default for convenience.

Execution and arithmetic

Slide 9 — The execution mechanism of classical Prolog

Three rules, and that is the whole thing:

  • Always execute the calls in the body of a clause left to right.
  • On entering a procedure, if several clauses unify — a choice point — take the first unifying clause, that is, the leftmost unexplored branch.
  • On failure, backtrack to the next unexplored clause of the last choice point.

:- 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).
?- grandparent(charles, X).
Expected answer:

X = george ?
Follow it on the tree: parent(charles,P) first tries father, giving P = philip; then parent(philip,G) has no solution at all, and the whole branch fails. Backtracking returns to the last choice point, takes mother(charles,ana), and parent(ana,G) then succeeds with george.

This is the point at which to start using the debugger, which walks you through exactly these steps. In the Ciao playground and in the emacs mode you can turn it on and follow call, exit, redo and fail on every goal. Reading about backtracking is one thing; watching the same goal be re-entered by redo is another.

Slides 10–12 — Built-in arithmetic

Prolog has had built-in arithmetic since the beginning, and it is not Peano arithmetic: it is an interface to the arithmetic the CPU already knows how to do — machine integers and floating point.

It is essential to understand what this costs. The pure versions we wrote in the previous part are reversible: plus/3 runs as a minus, times/3 runs as a division. Prolog's built-in arithmetic is a functional evaluator of arithmetic terms. It is not logical and it is not reversible.

The type of arithmetic terms. It is defined exactly the way we defined types in the pure part:

  • a number is an arithmetic term;
  • if f is an n-ary arithmetic functor and X1,...,Xn are arithmetic terms, then f(X1,...,Xn) is an arithmetic term.
The arithmetic functors are +, -, *, / (float quotient), // (integer quotient), mod, and many more. So (3*X+Y)/Z is fine provided that when it is evaluated X, Y and Z are themselves arithmetic terms; and a+3*X will raise an error, because a is a constant with no numeric value.

Slide 11 gives the type itself, and it is worth looking at as a type: a long but perfectly ordinary recursive definition, in the same shape as nat/1 or list/1, where every alternative but the first is built from smaller arithmetic expressions. Its real name in Ciao is arithexpression/1 (core/engine/arithmetic.pl defines it); the slide — and the definition below — write arithexpr for short, to fit.

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]
    | ...
num is the base case — a number is an arithmetic term — and [arithexpr] is the one that looks odd until you remember that "a" is a list of character codes: a one-element list evaluates to its element, which is how X is "a" gives 97.

Beyond those there are rem, mod, gcd, the shifts and the bitwise operators, and integer, truncate, floor, round, ceiling. The manuals have the complete list.

An arithmetic term is still just a term. Nothing about it is special until something evaluates it. write_canonical/1 makes this concrete:

?- X = 3+4/2, write_canonical(X), nl.
Expected answer:

+(3,/(4,2))

X = 3+4/2 ?
X is bound to a data structure. Nothing was computed.

The two things you can do with an arithmetic term are evaluate it and compare it.

Z is X evaluates X — which must be an arithmetic term — and unifies the result with Z. The comparison operators <, >, =<, >=, =:= (arithmetic equality) and =\= (arithmetic inequality) evaluate both arguments and compare the results.

A mnemonic for the comparison operators, which trips up everyone coming from another language: it is =<, not <=. Why? Because <= looks like a nice arrow, and arrows are far too useful to waste on an arithmetic operator — they are wanted for implication. So the rule is: if it looks like a nice arrow, it is the wrong one. (>= is fine, because there is no confusion there.)

?- X is 3*3//2.
Expected answer:

X = 4 ?
?- Z = 3//2, X is 3+Z.
Expected answer:

X = 4,
Z = 3//2 ?
Look carefully at that second one. Z is bound to the term 3//2 — not to 1. The evaluation happens only at the is, and at that moment the whole structure 3+Z is evaluated, Z and all:

?- X = 1 + Y, Y = 3/2, Z is X.
Expected answer:

X = 1+3/2,
Y = 3/2,
Z = 2.5 ?
Y is not 1.5 and is not a rational number; it is the term /(3,2). is/2 interprets the structure handed to it, and only then does arithmetic happen.

Failure versus error

These two outcomes are very different, and the slide separates them carefully.

Failure — the goal is well formed, evaluates fine, and simply is not true. The system backtracks as usual:

?- X=3, Y=4, Y < X+1.
Expected answer:

no
?- X=3, Y=4, X is Y+1.
Expected answer:

no
Error — the goal cannot be evaluated at all, and execution is aborted:

?- Y=4, Y < a+1.
Expected answer:

{ERROR: arithmetic:is/2, arg 2 - expected an arithmetically evaluable
 expression, found /(a,0)}
Read that message; it is a good one. The predicate is is/2 (which stands in for all the arithmetic operators) in module arithmetic; the problem is in argument 2; it expected something evaluable and found a — printed in name/arity form as /(a,0), i.e. the atom a.

?- X is Z+1.
Expected answer:

{ERROR: arithmetic:is/2, arg 2 - argument is not sufficiently instantiated}
An unbound variable has no value, so there is nothing to evaluate. Note that the pure version would have worked perfectly: plus(Z,s(0),Y) is reversible and would have generated candidates. is/2 cannot, because it is a function.

?- X=3, X =:= f(a).
Expected answer:

{ERROR: arithmetic:=:=/2, arg 2 - expected an arithmetically evaluable
 expression, found /(f,1)}
This is not the only arithmetic available. Written as a constraint, Y .=. Z+1 does not fail and does not raise an error: it answers Y = 1+Z, which is the correct and complete answer, given that nothing more is known. And 3 .=. Z+1 answers Z = 2 — running "backwards" without a complaint.

So Peano arithmetic was declarative but slow; is/2 is fast but not declarative; constraints are both. That is the subject of the last part of the course, and one of the things constraint logic programming does is give arithmetic its declarativeness back.

Slide 13 — Arithmetic programs

The first casualty is plus/3:

plus(X,Y,Z) :- Z is X + Y.
It works in one direction only — X and Y must be bound to arithmetic terms. We have lost the recursive structure of the numbers, and with it reversibility. (The meta-logical predicates of a later slide let you recover some of it, by testing which arguments are bound and choosing what to compute.) What we have won is a great deal of performance.

Factorial makes the trade visible. Here are both versions side by side:

:- module(_,_).

% a) Using Peano arithmetic:

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).

% 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.
The two are the same algorithm. The Peano one is pure and reversible; try it on 5 and it is fine, on 6 it takes a noticeable moment, on 7 you stop waiting.

?- factorial(7,F).
Expected answer:

F = 5040 ?
Instantaneous — and so is factorial(1000,F), whose answer has 2568 digits. Imagine that number built out of nested s/1 terms; that is why the Peano version cannot be printed, never mind computed.

Goal order now matters in a new way

In pure logic programming, moving a goal changed the efficiency and sometimes the termination. With is/2 it can change a working program into one that raises an error. The temptation is to make factorial tail recursive by putting the recursive call last:

wrong_factorial(0,1).
wrong_factorial(N,F) :-
    N > 0,
    N1 is N-1,
    F is F1*N,              % F1 has no value yet!
    wrong_factorial(N1,F1).
?- wrong_factorial(3,F).
Expected answer:

{ERROR: arithmetic:is/2, arg 2 - argument is not sufficiently instantiated}
At the moment F is F1*N is reached, F1 is a fresh variable — the recursive call that will bind it has not run yet. In pure logic programming this would have been survivable: the pure times/3 would have generated candidates and the recursive call would have checked them, inefficiently but correctly. is/2 cannot do that.

The same reason kills the reverse direction:

?- factorial(N,6).
Expected answer:

{ERROR: arithmetic:>/2, arg 1 - argument is not sufficiently instantiated}
factorial/2 with the built-in arithmetic is a function, not a relation.

Exercises

Exercise: factorial with an accumulating parameter — opens in the Ciao playground, the same link as the button on the slide.

Types, structures and terms

Slide 14 — Dynamic checking of basic types

Why does Prolog need built-in type tests at all? In the pure part we did not: because the numbers were terms, the type "natural number" was an ordinary predicate we could write down, nat/1. But the built-in numbers are not terms — they are supported by the underlying machine — so we also need opaque tests for them.

They are unary relations that check the type of a term:

  • integer(X)
  • float(X)
  • number(X)
  • atom(X) — a non-variable term of arity 0 other than a number
  • atomic(X) — an atom or a number
and so on. Together they form a small lattice: number covers integer and float; atom and number are disjoint; atomic sits above both.

They behave as if defined by a (possibly infinite) table of facts — integer(1). integer(2). … — and they either succeed or fail; they never raise an error.

But they cannot generate. nat(X) in Peano arithmetic starts producing 0, s(0), s(s(0))… integer(X) on an unbound X simply says no. It does not enumerate the integers; for this built-in, a variable is not an integer.

That behaviour is outside first-order logic, because what it really tests is the instantiation state of a variable — a question that cannot even be phrased in logic.

?- integer(1).
Expected answer:

yes
?- integer(1.0).
Expected answer:

no
?- integer(X).
Expected answer:

no
A reliable test for impurity: does the order matter?

?- X = 1, integer(X).
Expected answer:

X = 1 ?
?- integer(X), X = 1.
Expected answer:

no
Two orders, two different answers — so integer/1 is not a pure predicate. Contrast nat/1: X = s(s(s(0))), nat(X) and nat(X), X = s(s(s(0))) both succeed. The second one has to backtrack through 0, s(0), s(s(0)) before it gets there — less efficient, but logically consistent, which is the whole point.

Is integer/1 therefore wrong? No — it is impure, and you have to know it. If you want a pure integer test, use Peano numbers or constraints. If you use these, then that part of your program can no longer be read as logic; you are reading an imperative program.

Slide 15 — Recovering some reversibility

Here is what the type tests are actually good for. Recall that

plus(X,Y,Z) :- Z is X + Y.
only runs in one direction. Guard the clauses with number/1 and you can cover three:

:- module(_,_).

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

plus2(X,Y,Z) :- number(X), number(Y), Z is X + Y.
plus2(X,Y,Z) :- number(X), number(Z), Y is Z - X.
plus2(X,Y,Z) :- number(Y), number(Z), X is Z - Y.
?- plus2(4,5,Z).
Expected answer:

Z = 9 ?
?- plus2(X,5,9).
Expected answer:

X = 4 ?
Give it any two of the three and it computes the third. But partitioning a number into two others is beyond it:

?- plus2(X,Y,9).
Expected answer:

no
And no is the wrong answer — it is not true that no two numbers add up to 9. Peano arithmetic would have started generating pairs. What this program should do is raise an error saying it is not sufficiently instantiated (which needs a cut and a catch-all clause, both of which come later).

The honest answer, of course, is neither no nor an error: it is the equation X = 9-Y. That is exactly what a constraint system answers, and if you later add Y = 2 the equation resolves itself. By the time you have handled enough cases here by hand, you will have written a small part of constraint logic programming yourself. Which is the real solution, and the last part of the course.

Slide 16 — Structure inspection with functor

functor(X, F, A) relates a term to its functor name and arity, and works in both directions:

  • given X = f(X1,...,Xn), it gives F = f and A = n;
  • given the atom f and the integer n, it builds X = f(_,...,_) with n fresh arguments.
It raises an error if X and either F or A are variables, or if A is not a non-negative integer; it fails if unifying an output with a given value fails.

?- functor(t(b,a), F, A).
Expected answer:

A = 2,
F = t ?
?- functor(Term, f, 3).
Expected answer:

Term = f(_,_,_) ?
functor(Vector, v, 100) builds a hundred-argument term in one step — which is the beginning of an array. (In some systems the arity is limited to 256 by default; there are libraries for unbounded arrays.)

Slide 17 — Structure inspection with arg

arg(N, X, Arg) unifies Arg with the N-th argument of the compound term X. It gives access to a structure argument in constant time and very compactly. It raises an error if N is not an integer, or X is not compound or is a free variable; it fails for N = 0, for N beyond the arity, or if the unification fails.

?- T = date(9,'February',1947), arg(3,T,X).
Expected answer:

T = date(9,'February',1947),
X = 1947 ?
The same result comes from plain unification — T = date(_,_,X) — and where you have a fixed, known shape that is the better style. arg/3 earns its keep when the position is computed, as in the array code below.

Building and filling in one go:

?- functor(Array,array,5), arg(1,Array,black), arg(5,Array,white).

Array = array(black,_,_,_,white) ?
A question worth stopping on, from the slide: what does arg(2,[a,b,c,d],X) return?

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

X = [b,c,d] ?
Not b. Because a list is not a special data type: [a,b,c,d] is the term '.'(a,[b,c,d]), a binary structure whose second argument is the tail. Confirm it directly with functor([a,b,c,d],F,A), which answers F = '.', A = 2.

Slides 18–19 — Example: arrays

With functor/3 and arg/3 you can treat a term as an array. Here is array addition:

:- module(_,_).

add_arrays(A,B,C):-
    functor(A,array,N), % gets length N from array A
    functor(B,array,N), % checks that B has the same length
    functor(C,array,N), % creates 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).

% 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).
?- add_arrays(array(1,2,3), array(4,5,6), R).
Expected answer:

R = array(5,7,9) ?
Note the elegance of the length check: writing the same N in all three functor/3 calls both reads the length off A, checks that B has it, and creates C with it. Walk through it once, running forwards with the first two arrays given:

  • functor(A,array,N): A is instantiated, so this checks the principal functor is array and reads the arity into the free N.
  • functor(B,array,N): now N is bound, so this checks that B has the same length. That coincidence of variable is the whole length check.
  • functor(C,array,N): C is free but array and N are bound, so functor/3 runs backwards and creates the result array — length N, full of holes.
That third call matters, because arg/3 does not create anything; it fails on an unbound term. So the result array must exist, full of variables, before the loop starts filling it.

The subtle part is what arg(I,C,CI) gives you. Logical variables are declarative pointers, so CI is not a copy of anything: it is the very variable sitting in slot I of C. When CI is AI + BI later binds it, the value appears inside the array. There is no write-back step, because there was never a copy.

This is what unification buys: a formal theory that gives you these memory operations directly, with no need to model the memory yourself.

Note also that in the recursive call the three arrays are passed unchanged — the same pointers go round every time. It is the index that moves.

Mismatched lengths simply fail:

?- add_arrays(array(1,2,3), array(4,5), R).
Expected answer:

no
The slide then asks: in the list version, where is the check that the three lists have equal length? There is no functor/3 call and no N anywhere. The answer is that it is in the clause heads: add_lists([],[],[]) requires all three to end at the same time, and the recursive clause requires all three to have a head and a tail. The type of the data is doing the checking, exactly as it did throughout the pure part.

Which version is better? The list one is shorter and easier to write — a recursion is a more natural way to express this than a loop, and a loop always needs an extra variable that has nothing to do with the problem. The array version wins when you want random access: jump to the first element, then the last, then back — arg/3 does that in constant time and a list does not.

There is one thing to be said for the verbose loop, though: you can see exactly what it does. In an imperative language it is not always obvious whether the test happens before or after the body, or where the decrement lands. Here all of it is written out in front of you.

Running it backwards does not work, and the reason is instructive:

?- add_arrays(array(1,2,3),A2,array(5,7,9)).

{ERROR: arithmetic:is/2, arg 2 - argument is not sufficiently instantiated}
By the time the loop reaches CI is AI + BI we are giving CI and AI and asking for BI, and is/2 cannot run backwards. The fix is the one from slide 15: replace is/2 with the guarded plus2/3, and array subtraction starts working too.

What it still will not do is take one array and enumerate all the pairs of arrays that add to give it. For that you would need Peano arithmetic — which you could write — or a constraint, which is reversible arithmetic and runs in every direction. That is the last part of the course.

Slide 19 (contd.) — Syntactic sugar for arg

arg/3 is fast but it reads badly: three calls and two temporary variables to say C[I] = A[I]+B[I]. Ciao's functional syntax lets you fix that yourself, in five lines:

:- module(_,_).

:- use_package(fsyntax).  % use functional notation
:- op(250,xfx,@).         % define @ as an infix operator
T@N := A :- arg(N,T,A).   % define @ to call arg/3
:- fun_eval @ / 2.        % evaluate all occurrences of @/2
array(A,N) :- functor(A,array,N).
:- fun_eval arith(true).  % evaluate arithmetic expressions (I-1)

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,
    add_els(I-1,A,B,C).
?- add_arr(array(1,2,3), array(4,5,6), R).
Expected answer:

R = array(5,7,9) ?
C@I is A@I + B@I — which is what we wanted to write in the first place. The point is not the convenience; it is that the notation was added by the user, with an operator declaration and one clause, and the rest of the language did not have to know about it.

And nothing forces @. Declare sub as an xfx operator and write a sub 3; or use : and write a:3. With functional notation you can go further and chain them — a sub 3 sub 2 reads element (3,2) of a matrix — because the syntax is extensible and you decide how convenient you want it to be.

Slide 20 — Example: subterms of a term

subterm(Sub,Term) should hold when Sub occurs somewhere inside Term. Two clauses say it all:

:- module(_,_).

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 is 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 is a subterm 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).
The base case is by definition: anything is a subterm of itself, and "is" here means unifies with. The recursive case walks into the structure: take the arity with functor/3, use n_to_one/2 to generate an argument position J — from N down to 1, on backtracking — pick that argument out with arg/3, and recurse. nonvar/1 stops it from trying to take a free variable apart.

?- subterm(f(a), g(b,f(a))).
Expected answer:

yes
?- subterm(f(b), g(b,f(a))).
Expected answer:

no
And now the part worth pausing on. Because functor/3 and arg/3 both work backwards well enough, the same program enumerates the subterms:

?- subterm(X, g(b,f(a))).
Expected answer:

X = g(b,f(a)) ? ;
X = f(a) ? ;
X = a ? ;
X = b ? ;
no
Four subterms, in that order — the whole term, then down into the second argument (n_to_one/2 counts down, so the last argument comes first), then the first. Ask yourself how many lines this would take in another language, and whether that version would also generate.

This program repays running under the debugger. The interesting moment is what happens when the recursion reaches a constant: functor(a,F,N) gives F = a and N = 0 — a constant is a term of arity zero — and then n_to_one(0,J) fails, because 0 > 0 is false and 0 > 1 is false. That is how the descent stops.

A historical note, and a lesson about testing. An older version of this program had no n_to_one/2 and relied on arg(0,...) failing to stop the recursion. That worked for years. In current Ciao, arg/3 still fails for index 0 on a compound term, but it now raises an error when the second argument is atomic — so the old version aborts instead of answering. The version above is the fixed one.

Slide 21 — Higher-order structure inspection: univ

T =.. L — read it aloud as "univ", never as "equals dot dot". L is the decomposition of the term T into a list whose head is the principal functor and whose tail is the arguments.

?- date(9,february,1947) =.. L.
Expected answer:

L = [date,9,february,1947] ?
A mnemonic for which side is which: think of the term being spilled into the list. The side it spills into is the list.

The interesting direction is backwards — from a list of pieces, build a term:

?- F = '+', X =.. [F,a,b].
Expected answer:

F = +,
X = a+b ?
Look at what just happened: F is a variable, and it ended up as the functor of the term. Push it one step further and the term gets evaluated:

?- F = '+', X =.. [F,3,5], Z is X.
Expected answer:

F = +,
X = 3+5,
Z = 8 ?
Functors can now be variables — so we are no longer in first-order logic. That is exactly what makes univ powerful enough for higher-order programming and meta-programming, and it is also the reason to be careful: outside first-order logic you lose the guarantees that came with it.

Use it only when strictly necessary. It is expensive in both time and memory.

Extending deriv with the chain rule

In the pure part we differentiated sums, products, quotients, powers and logarithms — but not compositions, because for that you must take the expression apart, and we had no way to do it. With univ we do:

:- module(_,_).

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).
The chain rule says the derivative of f(g(x)) with respect to x is the derivative of f(g) with respect to g, times the derivative of g with respect to x. The univ call is what extracts the inner function G_X out of the composite term — the functor is discarded with _, because the rule does not care which outer function it is.

?- deriv(sin(cos(x)), x, D).
Expected answer:

D = cos(cos(x))* -sin(x) ?
The outer derivative cos(cos(x)), times the inner one -sin(x).

Exercises

Exercises: univ, functor and arg in terms of each other — opens in the Ciao playground, the same link as the button on the slide.

Slide 22 — Strings, atoms, and creating new atoms

An atom is a compact, indivisible entity; a string is a list of character codes. They are different things, and name/2 converts between them.

?- name(hello, S).
Expected answer:

S = [104,101,108,108,111] ?
?- name(A, "hello").
Expected answer:

A = hello ?
(With set_prolog_flag(write_strings,on) the first answer prints as S = "hello".)

Backwards, name/2 creates atoms out of nothing — and combined with univ that means creating new functors, and therefore new structures, at run time:

?- name(N, "foo"), T =.. [N,b,c].
Expected answer:

N = foo,
T = foo(b,c) ?
That is meta-programming: building new symbols, identifiers and functors while the program runs.

name/2 is ambiguous. The atom '1' and the number 1 have the same name, [49] — but they are different terms and do not unify. Going forwards that costs nothing; going backwards name/2 always gives you the number when the characters spell one:

?- X='1', atom(X), name(X,S), name(Y,S), number(Y).
Expected answer:

S = [49],
X = '1',
Y = 1 ?
We started with an atom and ended with a number, and the query succeeded — which is exactly the problem. The ISO standard fixes it by splitting name/2 into two unambiguous predicates:

?- atom_codes(X, "1").
Expected answer:

X = '1' ?
?- number_codes(X, "1").
Expected answer:

X = 1 ?
You choose which one you want; there is nothing left to guess.

Meta-logical predicates and term comparison

Slide 23 — Meta-logical predicates

These ask about the instantiation state of a term — a question that cannot be phrased inside first-order logic, which is exactly why they are called meta-logical.

  • var(X) succeeds iff X is a free variable
  • nonvar(X) succeeds iff X is not a free variable
  • compound(X) succeeds iff X is instantiated to a compound term
  • ground(X) succeeds iff X is fully instantiated — contains no variables anywhere
?- var(X), X = f(a).
Expected answer:

X = f(a) ?
?- X = f(a), var(X).
Expected answer:

no
Same two goals, opposite order, different answers — the impurity test from the previous section, applied again. nonvar/1 behaves as the exact mirror image.

?- X = f(Y), compound(X).
Expected answer:

X = f(Y) ?
?- X = f(Y), ground(X).
Expected answer:

no
f(Y) is compound but not ground, because Y is still free. A ground (or closed) term is one with no variables inside it at all.

What are they for? Two things: to control goal order for efficiency, and to restore some flexibility to programs that use non-reversible built-ins. The plus2/3 of slide 15 could have been written with nonvar/1 instead of number/1 — though number/1 is better there, because it also checks that what arrived is actually a number.

Slides 24–25 — Choosing an implementation by calling mode

Before anything else: this is not necessary. The obvious definition of list length already runs in both directions.

:- module(_,_).

len([],0).
len([_|T],N) :- len(T,TN), N is TN+1.
?- len([a,b,c], N).
Expected answer:

N = 3 ?
?- len(L, 3).
Expected answer:

L = [_,_,_] ?
A list of three holes — just as functor/3 gave us an array of holes. So do not assume that a program stops being reversible the moment it contains an is/2.

What we can do, though, is pick a different implementation depending on the direction, for efficiency. That is what the meta-logical predicates make possible:

:- module(_,_).

mylength(Xs,N):-
    var(Xs),
    integer(N),
    create_list(N,Xs).
mylength(Xs,N):-
    nonvar(Xs),
    compute_length(Xs,N).

create_list(0,[]).
create_list(N,[_|Xs]):-
    N > 0,
    N1 is N - 1,
    create_list(N1,Xs).

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).
?- mylength([a,b,c], N).
Expected answer:

N = 3 ?
?- mylength(L, 3).
Expected answer:

L = [_,_,_] ?
The first clause fires when Xs is unbound and N is an integer: there is no list to walk, so it loops on the number instead, laying down one cons cell per iteration until it hits zero.

Why bother? Because create_list/2 is tail recursive — the recursive call is last, so the local variables are dead by the time it is made, the stack frame can be discarded, and the whole thing runs as a loop in constant memory. len/2 in that direction is not: its is/2 sits after the recursive call, so computing a length of a million pushes a million frames before any of them can return.

The second direction gets the same treatment, with an accumulating parameter. compute_length_/3 carries a counter that starts at 0 and grows by one per cons cell; the third argument is passed untouched all the way down and receives the answer at the base case. The addition now happens before the recursive call, so this version is tail recursive too.

And that is the moral. We started from a definition so short it is essentially the specification of what a list length is — and we can run that specification, in either direction. Then, in the same language, we refined it into a version where both directions are linear, tail recursive and allocate no stack, as efficient as two hand-written imperative programs would be.

Specification, executable specification, and efficient program — all in one notation. That is something Prolog is genuinely good at.

Slide 26 — Comparing non-ground terms

Many programs need to compare terms that are not ground and not numbers. Unification will not do, because unification instantiates: f(X) = f(a) does not merely observe that the two could be made equal, it goes ahead and makes X be a.

Identity tests just look:

  • X == Y (identical)
  • X \== Y (not identical)
?- f(X) == f(X).
Expected answer:

yes
?- f(X) == f(Y).
Expected answer:

no
This is worth digesting slowly, because it tells you something real about the implementation.

Are X and X the same variable? Yes. Are X and Y? No. Now unify them and ask again:

?- X = Y, X == Y.
Expected answer:

Y = X ?
Yes — so ==/2 knows whether two variables have been aliased. Read in terms of the pointer picture from the previous deck: ==/2 tells you whether two pointers point at the same place. Which means you can inspect aliasing at run time, and write something very close to imperative data structure code.

Term ordering gives a total order over all terms — which is algorithmically very convenient, because unless two terms are identical, one of them is definitely the larger:

  • X @> Y, X @>= Y, X @< Y, X @=< Y
Numbers order numerically, atoms alphabetically:

?- f(a) @> f(b).
Expected answer:

no
?- f(b) @> f(a).
Expected answer:

yes
The comparison is recursive and works to any depth, on a structure of any size, without your having to navigate it.

The rule for compound terms is arity first, then name, then arguments left to right. So:

?- f(a,b) @> g(a).
Expected answer:

yes
yes, even though g comes after f alphabetically — because f(a,b) has arity 2 and g(a) has arity 1, and arity is compared first. With equal arities the name decides, and f(a,b) @> g(a,b) is no as expected.

Where do variables sit in the order? They come before everything else, and among themselves the order is not by name — the name is only for us to read; a variable might just as well be called _33. It is the order of creation: variables created earlier are smaller.

The reason is the implementation. Variables live in the heap, and comparing them compares their heap addresses — which is precisely the information a system wants, since a younger variable will be deallocated sooner on backtracking.

The standard calls this implementation dependent, and it is; but most systems, Ciao among them, behave this way.

Slide 27 — Two applications of term comparison

Revisiting subterm, for non-ground terms

Recall subterm/2 from slide 20, whose base case was subterm(Term,Term) — that is, unifies with. Ask it whether f(a) is a subterm of g(X,k):

:- module(_,_).

subterm(Term,Term).
subterm(Sub,Term):-
    nonvar(Term),
    functor(Term,_F,N),
    n_to_one(N, J),
    arg(J,Term,Arg),
    subterm(Sub,Arg).

subterm_ng(Sub,Term) :- % a) identical, rather than unifiable
    Sub == Term.
subterm_ng(Sub,Term) :-
    nonvar(Term),
    functor(Term,_F,N),
    n_to_one(N, J),
    arg(J,Term,Arg),
    subterm_ng(Sub,Arg).

n_to_one(N, N) :- N > 0.
n_to_one(N, X) :- N > 1, N1 is N-1, n_to_one(N1, X).
?- subterm(f(a), g(X,k)).
Expected answer:

X = f(a) ?
It says yes, and instantiates X to make it so. Which answer is right depends entirely on what you are doing. Logically it is impeccable: f(a) can be a subterm of g(X,k), and this is what you want if the program is to run backwards. But for meta-programming, where you care about the variables themselves, you want a no: f(a) does not literally occur there.

Change the base case from unification to identity and you get the other reading:

?- subterm_ng(f(a), g(X,k)).
Expected answer:

no
?- subterm_ng(f(a), g(h(f(a)),k)).
Expected answer:

yes
On ground terms the two behave identically. They part company exactly where a variable would have been instantiated.

Inserting into an ordered list

:- module(_,_).

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).

% The same program written with unification and arithmetic comparison:

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).
?- insert_ee([a,b,e], c, L).
Expected answer:

L = [a,b,c,e] ?
Because @> and @< order any terms, this works on symbols and not just numbers. Now compare with the version that uses =/2 and the arithmetic comparisons:

?- insert_unif([a,b,e], c, L).
Expected answer:

{ERROR: arithmetic:>/2, arg 1 - expected an arithmetically evaluable
 expression, found /(a,0)}
>/2 tried to evaluate the atom a. And the difference shows up again with an unbound item:

?- insert_ee([a,b,e], X, L).
Expected answer:

L = [X,a,b,e] ?
?- insert_unif([a,b,e], X, L).
Expected answer:

L = [a,b,e],
X = a ?
insert_ee/3 places the unbound X at the front — correctly, since variables precede atoms in the standard order. insert_unif/3 instead unifies X with a and declares the item already present. It is the == versus = distinction, doing visible damage.

Input/output

Slides 28–29 — The I/O predicates

There is a whole literature on declarative input/output in logic and functional programming — monadic I/O and much else. What follows is the plain, standard set that has been in Prolog from the beginning and is in the ISO standard, and which every system has.

Stream control — where are we reading from and writing to:

predicate what it does
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 — and this is the powerful group:

predicate what it does
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, for binary work:

predicate what it does
put_code(N)write the character with code N
get_code(N)read the next character code and unify it with N

read/1 means you never have to write a parser. It reads the characters in the file and hands you back a data structure. Not a string of the text — the term itself.

?- read(X), length(X,N), write(N). typed at the prompt, answered with [1,2,3,4,5]., writes 5. Five, not the number of characters — because what came back was the list.

And write/1 is the exact converse. Hand it a pointer into the middle of an arbitrarily complicated structure and it prints whatever is there, to any depth, with no navigation code on your part.

This is worth a moment's thought when you are choosing a data file format. If you write your data as Prolog terms, you get the reader for free — and operators mean the notation can be made to look like whatever you want. You need no CSV, no JSON parser, no marshalling library. Make the file as indented and as ugly as you like; read/1 handles it.

Stream-based versions of all of these also exist, and are closer to what other languages do: open(File,Mode,S) with Mode one of read, write or append, and close(S); then write(S,X), nl(S), read(S,X), put_code(S,N), get_code(S,N). Use these when you need several files open at once — each gets its own stream identifier.

Two things to remember when writing terms you intend to read back

The full stop. A term is terminated by a dot, exactly as in a source file. write(a+b) without a following write('.') produces a file that read/1 refuses, with an "operator expected after expression" error.

Operators. write/1 uses whatever operator declarations are in force, so a+b is written a+b — which only reads back if the same declarations are in force at reading time. write_canonical/1 writes without operators:

?- tell(bar), write(a+b), write('.'), told.        % bar contains:  a+b.
?- tell(baz), write_canonical(a+b), write('.'), told.  % baz contains: +(a,b).
The canonical form is the safe one: it can always be read back, whatever operators happen to be declared.

Slide 30 — An example, and a warning

:- module(_,_).

:- use_module(library(stream_utils)).

write_list_to_file(L,F) :-
    telling(OldOutput),             % grab the current output stream
    tell(F), write_list(L), told,   % write into F, close
    tell(OldOutput).                % restore the previous output stream

write_list([]).
write_list([X|Xs]):-
    write(X),
    write('.'),
    nl,
    write_list(Xs).

show_file(F) :-
    file_to_string(F,L),
    format("~s",[L]).
?- write_list_to_file([f(a), m, g(k)], foo), show_file(foo).
Expected answer:

f(a).
m.
g(k).

yes
The telling/1 … tell(OldOutput) sandwich is the point of the example: the predicate may be called from a program that was already writing somewhere, and it puts the output stream back where it found it.

Reading the file back one term at a time:

?- see(foo), read(X), read(Y), read(Z), read(W), seen.

W = end_of_file,
X = f(a),
Y = m,
Z = g(k) ?
The fourth read returns the atom end_of_file. Reading past that raises a past-end-of-stream error, as in any other language.

More powerful, format-based predicates exist — format/2 and format/3, familiar from other languages; the libraries have a great deal more. But do not lose sight of the essential point:

All of these input/output predicates are side effects. They are not logical, their order matters absolutely, and backtracking does not undo them. Everything the pure part of the course said about reading a program as a set of logical statements stops applying the moment one of these appears.

The cut

Slides 31–32 — The pruning operator

The cut, written ! and of arity zero, goes wherever a goal goes. It is the standard pruning operator: it removes parts of the search tree.

The cut, when executed, commits Prolog to all the choices made since the current call to the predicate in which the cut appears.

Read that "when executed" carefully — it is the single most important thing about the cut. A cut is not a declaration or an annotation. It is a goal that runs, and it does nothing at all unless control actually reaches it. The question is never is there a cut in this clause; it is always did we execute it.

When executed, a cut prunes exactly two things:

  • all clauses below the clause the cut appears in, and
  • all alternative solutions to the goals to its left in that clause.
It does not affect the goals to its right; those still backtrack normally among themselves.

:- module(_,_).

s(1) :- write('\n Clause 1 of s/1 succeeded.').
s(2) :- write('\n Clause 2 of s/1 succeeded.').

l(a) :- write('\n Clause 1 of l/1 succeeded.').
l(b) :- write('\n Clause 2 of l/1 succeeded.').

r(8) :- write('\n Clause 1 of r/1 succeeded.').
r(9) :- write('\n Clause 2 of r/1 succeeded.').

m(e) :- write('\n m/1 succeeded.').

p(X,_Y) :-
    l(X),
    write('\n Reached end of clause 1 of p.').
p(X,_Y) :-
    r(X),
    !,    % Cut
    write('\n Executed cut (no more alternatives of r/1 or p/1)'),
    write('\n At end of clause 2 of p.').
p(X,_Y) :-
    write('\n Reached clause 3 of p.'),
    m(X).
Run ?- s(A), p(X,Y). and ask for more solutions. The write/1 calls make the path visible.

Trace it by hand first. s(A) succeeds with A = 1. In p, the first clause tries l(X); suppose that eventually fails. The second clause calls r(X), which succeeds with X = 8, and then the cut is executed. At that moment:

  • r(9) disappears — it is an alternative of a goal to the left of the cut;
  • the third clause of p/2 disappears — it is below the clause containing the cut.
So if we now fail, where do we go? Not to r, and not to p's third clause. We go to the last choice point before the call to p — which is s(2), untouched, because it was never inside p.

When the cut does nothing. If l(X) fails, we enter the second clause, and r(X) also fails, then the cut is never reached. Execution proceeds to the third clause exactly as if the cut were not there. Again: it is not the presence of the cut, it is the execution of it.

The operational one-liner. Once you have executed the cut in a clause, failing afterwards takes you straight out to your parent — to whoever called this predicate. Goals to the right of the cut may still backtrack among themselves; but when failure reaches back past the cut, it leaves.

Slide 33 — White and green cuts

Not all cuts are equally dangerous, so there is a traditional colour classification.

White cuts — do not discard solutions

:- module(_,_).

white_max(X,Y,X) :- X > Y, !.
white_max(X,Y,Y) :- X =< Y.
Remove the cut and nothing changes: the two conditions are mutually exclusive, so once X > Y has succeeded, the second clause is going to fail anyway. The cut merely says so in advance.

White cuts affect neither completeness nor correctness. Use them freely. In many cases the system introduces them for you; writing them yourself also documents that the clauses are exclusive.

They matter more than they look. What you least want in a logic program is useless choice points sitting on the choice-point stack. Leaving them there is the backwards equivalent of writing a non-tail-recursive predicate: every call pushes a record for a return you will never take.

Green cuts — discard correct solutions you did not want

:- module(_,_).

address(X,Add) :- home_address(X,Add), !.
address(X,Add) :- business_address(X,Add).

home_address(john,pfluggerville).

business_address(john,austin).
business_address(mary,boston).

member_normal(X,[X|_]).
member_normal(X,[_|T]) :- member_normal(X,T).

member_check(X,[X|_]) :- !.
member_check(X,[_|T]) :- member_check(X,T).
address/2 gives you one address, preferring the home one — the clause order becomes a priority order. Without the cut you would get both.

member_check/2 is the classic. member_normal/2 gives every occurrence, repeats included; often you only want to know whether something is there:

?- member_normal(1,[1,2,1,4]), write('found solution'), nl, fail.
Expected answer:

found solution
found solution

no
?- member_check(1,[1,2,1,4]), write('found solution'), nl, fail.
Expected answer:

found solution

no
fail/0 is a goal that always fails — and note that it is not impure; a = b would do the same job. Driving the search with write, nl, fail is the standard way to print every solution.

The first version finds 1 twice, because it occurs twice in the list. The cut in the base case of member_check/2 kills the alternative that the base case would otherwise have left behind, so the second occurrence is never reached.

Green cuts affect completeness but not correctness: every solution you get is right, you just do not get all of them — which is what you asked for. Necessary in many situations, but stay awake.

Slide 34 — Red cuts

Red cuts discard solutions that are incorrect according to the intended meaning. They are the ones that will drive you crazy, because at first you think you understand them.

Here is the first red cut everyone writes:

:- module(_,_).

red_max(X,Y,X) :- X > Y, !.   % Red cut!
red_max(_X,Y,Y).              % the X =< Y test is *missing*
The reasoning feels airtight, and it is the reasoning you would use in C: if X > Y we are done and we cut; otherwise we fell through, so X =< Y must hold, so why test it again?

?- red_max(5,2,M).
Expected answer:

M = 5 ?
?- red_max(5,2,2).
Expected answer:

yes
The maximum of 5 and 2 is 2. That is a wrong answer — not wrong of the system, which did exactly what was written, but wrong of the program.

Why does it happen? red_max(5,2,2) fails against the first head, because X cannot be both 5 and 2, so the cut is never executed. It then reaches the second clause, red_max(_X,Y,Y), which says nothing about the first argument and only requires the last two to be equal — and they are.

The second clause makes no sense on its own. You were reading it as "otherwise", which is a statement about the first clause, and you were reading both with one calling mode in mind: max(+,+,-). Give it three numbers and check, or run it backwards, and it collapses.

In pure logic programming you never have to think about this, because every clause means what it says independently. With cut you do.

Slide 35 — Living with red cuts

Tip one: delay the output bindings until after the cut. The damage above came from a unification that happened in the head, before the cut. Move it behind:

:- module(_,_).

red_max_delay_output(X,Y,M) :- X > Y, !, M = X.
red_max_delay_output(_X,Y,Y).

if_then_else_max(X,Y,M) :- ( X > Y -> M = X ; M = Y ).
?- red_max_delay_output(5,2,2).
Expected answer:

no
Correct now. The rule of thumb: if you see a repeated variable in the head of a clause containing a cut, you are unifying before the cut, and that is where the surprises come from.

Tip two: use if-then-else, which is syntactic sugar over exactly this pattern:

?- if_then_else_max(5,2,2).
Expected answer:

no
A note on case statements. Prolog has none, and does not need one — but notice what you assume when you write a case: that the first argument is an input and the second an output (a mode), and that exactly one branch applies. That is precisely the meaning of cut, so a case statement is

case(a,Y) :- !, Y = 1.
case(b,Y) :- !, Y = 2.
case(c,Y) :- !, Y = 3.
But you would normally write it as a table:

case(a,1).
case(b,2).
case(c,3).
which runs forwards and backwards. Write the cut version only when you actually want the case semantics.

Slide 36 — Red cuts: the leap year, four ways

The temptation here is the "rule with an exception": a year has 365 days, except leap years, which have 366.

:- module(_,_).

days_in_year(Y,366) :- leap_year(Y), !.
days_in_year(_Y,365).

leap_year(Y) :- number(Y), 0 is Y mod 4.
?- days_in_year(4,D).
Expected answer:

D = 366 ?
So far so good. Now:

?- days_in_year(4,365).
Expected answer:

yes
?- days_in_year(a,D).
Expected answer:

D = 365 ?
Year 4 apparently has both 366 days and 365, and the atom a is a year with 365 days.

Read the two clauses as logic, which is the whole point of the notation. The first says: a leap year has 366 days — true, no problem. The second says: any year has 365 days. Not "any other year" — any. So leap years satisfy both, and so does a. The program says exactly that, and the system is merely agreeing with you.

The best fix: make the cut white by writing the missing condition

:- module(_,_).

days_in_year_good(Y,D) :- leap_year(Y),  !, D = 366.
days_in_year_good(Y,D) :- standard_year(Y), D = 365.

leap_year(Y)     :- number(Y), 0 is Y mod 4.
standard_year(Y) :- number(Y), R is Y mod 4, R \= 0.
?- days_in_year_good(4,365).
Expected answer:

no
?- days_in_year_good(a,D).
Expected answer:

no
Both clauses are now true statements standing on their own, and the cut has become white: remove it and the answers do not change.

Delaying the output binding: better, but not enough

days_in_year_delay_output(Y,D) :- leap_year(Y), !, D = 366.
days_in_year_delay_output(_Y,365).
This fixes days_in_year_delay_output(4,365), which now correctly fails. But days_in_year_delay_output(Y,366) with Y unbound answers no — also wrong, since leap years exist. leap_year/1 starts with number/1, which cannot generate, so the first clause fails without reaching the cut, and the second cannot match 366.

The underlying problem is unchanged: we are writing for one mode, "give the year, get the days".

Checking the mode explicitly

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).
Now an unbound year is refused loudly instead of answered wrongly. days_in_year_moded(Y,366) prints the message and aborts.

Better still: say it with an assertion, and let the system do the checking:

:- use_package([assertions,modes,rtchecks]).

:- pred days_in_year_moded_assrt(+int,-int).

days_in_year_moded_assrt(Y,D) :- leap_year(Y), !, D = 366.
days_in_year_moded_assrt(_Y,365).
Called with an unbound year, run-time checking reports

Requires in *calls*:
    nonvar(_1)
But instead:
    var(_1)
which is the same protection, obtained by stating the intended mode rather than by programming around it — and the same assertion can also be checked statically, at compile time.

Exercises

Exercises on cut — opens in the Ciao playground, the same link as the button on the slide.

Two more examples worth working through

Splitting a list by sign — the max/3 pattern, now recursive:

:- module(_,_).

split([],[],[]).
split([X|L],[X|L1],L2) :- X >= 0, !, split(L,L1,L2).
split([X|L],L1,[X|L2]) :- split(L,L1,L2).       % should test X < 0
?- split([1,-1,3], L1, L2).
Expected answer:

L1 = [1,3],
L2 = [-1] ?
Read the clauses as logic. The base case is fine either way round: the positives and the negatives of an empty list are both empty. The second clause says if X >= 0, it goes in the first list. The third says in all cases X goes in the second list — so strictly it needs its own X < 0 test.

With that test present the cut is green: it prunes a branch that would have failed anyway, and the answers are unchanged. Drop the test, as above, and you are relying on the cut for correctness — which works while you call it forwards with a bound list, and stops working the moment you do anything else.

Where the cut does and does not reach. This one is subtle and worth predicting before running:

:- module(_,_).

k_1sol(j,k).
k_1sol(X,Y) :- k(X), !, z(Y).
k_1sol(l,m).

k(a).  k(b).
z(1).  z(2).
What are all the solutions of k_1sol(X,Y)?

?- k_1sol(X,Y).
Expected answer:

X = j, Y = k ? ;
X = a, Y = 1 ? ;
X = a, Y = 2 ? ;
no
Follow it. The first clause gives j,k with no cut involved. Ask for more, and the second clause runs: k(X) succeeds with X = a, and the cut executes — so k(b) disappears, and so does the third clause k_1sol(l,m). Then z(Y) runs and gives Y = 1.

Now ask for more again. z/1 still has an alternative — and the cut did not remove it, because z(Y) is to the right of the cut. So Y = 2 comes back. But there is no b,1 or b,2, because the cut did remove k(b).

A cut prunes backwards, never forwards. It cannot remove a choice point that did not exist when it ran — it has no predictive powers.

One value of k, all values of z. That is a lot of control for one character.

The summary for red cuts: they affect completeness, and they cost you the ability to reason about correctness from the declarative reading of the program. Avoid them when you can.

Meta-calls, higher order and aggregation

Slide 37 — Meta-calls and implementing higher order

call(X) turns a term X into a goal and calls it. X must be instantiated when the call is made, or an error is reported.

That one predicate is what makes meta-programming possible: interpreters, shells, negation (next section), and higher order.

:- module(_,_).

p(X) :- call(X).

q(a,1).
q(b,2).

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

maptuples(_P,[]).
maptuples(P,[Tuple|RTuples]) :-
    apply(P,Tuple),
    maptuples(P,RTuples).
?- G = q(a,Y), p(G).
Expected answer:

G = q(a,1),
Y = 1 ?
p/1 received a term and executed it as a goal.

Inventing apply

That last one is worth deriving rather than being handed, because the derivation is the whole idea of meta-programming in miniature.

Here is what you would like to write. Somebody gives you a value, X = a, and somebody else gives you a predicate symbol, Y = p. You want to call Y(X) — check whether a has property p, where the property arrives as data.

You cannot write that in standard Prolog. Y(X) is higher-order syntax and will not even parse. (Ciao, with the hiord package, does accept it — but that is an extension, not standard Prolog.)

Standard Prolog can still do it, using univ. Remember that univ spills a term into a list with the functor at the front — and that a list can contain variables. So build the term and then call it:

:- module(_,_).

:- use_module(library(lists)).
?- Y = p, P =.. [Y,a].
Expected answer:

P = p(a),
Y = p ?
P is now p(a), which has the right shape to be called. That is application of a predicate passed as data — we have just invented apply.

Now generalize. Suppose the symbol is member and the arguments are X and [1,2,3]. Writing them out works:

?- F = member, Args = [X,[1,2,3]], P =.. [F|Args].
Expected answer:

Args = [X,[1,2,3]],
F = member,
P = member(X,[1,2,3]) ?
But notice how easy the near-miss is. Put the argument list in as a single element rather than splicing it, and you get a predicate of arity one:

?- F = member, L = [X,[1,2,3]], P =.. [F,L].
Expected answer:

F = member,
L = [X,[1,2,3]],
P = member([X,[1,2,3]]) ?
member/1, with a two-element list as its only argument. Not what we wanted, and almost a typo.

The difference is [F|Args] versus [F,Args] — a cons, not a two-element list. Given Args as a list, putting the functor on the front with | is exactly what univ needs, whatever the arity. And that is why univ was designed with the list on the right-hand side.

?- F = append, Args = [X,Y,Z], P =.. [F|Args], call(P).
Expected answer:

Args = [[],Y,Y],
F = append,
P = append([],Y,Y),
X = [],
Z = Y ?
Three arguments this time, and it worked without changing anything — because Args can be any length. Wrap those two goals in a predicate and you have

apply(F,Args) :- G =.. [F|Args], call(G).
The language does not have apply/2. You wrote it, out of univ and call/1, in one line — and of course you can then put it in a library, which is what everyone does. Higher order was not designed into Prolog; it fell out of univ.

There is another route, incidentally, called a Warren transformation, and the call/n family of the next slide is a third. And once you have any of them, map, fold and the rest are ordinary library predicates.

Combining call/1 with univ this way gives us apply/2: assemble the goal out of the predicate name and the argument list, then call it.

?- P = q, apply(P,[a,Y]).
Expected answer:

P = q,
Y = 1 ?
The predicate arrived in a variable. And with apply/2 in hand, a map over a list of argument tuples is three lines:

?- maptuples(q,[[a,X],[b,Y]]).
Expected answer:

X = 1,
Y = 2 ?

Slide 38 — call/n and the higher-order library

There is a whole family: call/1, call/2, …, call/n. The first argument is a term that may already carry some of the arguments; the remaining ones are supplied as the extra arguments of call/n. This is much cheaper than univ, and much more readable.

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

:- use_module(library(lists)).
:- use_module(library(hiordlib)).

q(a,1).
q(b,2).

plus2(X,Y,Z) :- number(X), number(Y), Z is X + Y.
plus2(X,Y,Z) :- number(X), number(Z), Y is Z - X.
plus2(X,Y,Z) :- number(Y), number(Z), X is Z - Y.
?- P = q, call(P,X,Y).
Expected answer:

P = q,
X = a,
Y = 1 ?
?- P = q(a), call(P,Y).
Expected answer:

P = q(a),
Y = 1 ?
Both spellings work: hand over the bare predicate name and supply both arguments, or hand over a partially applied term and supply the rest. It works for library predicates just as well:

?- P = append, call(P,[1,2],[3,4],Y).
Expected answer:

P = append,
Y = [1,2,3,4] ?
?- P = append(X,Y), call(P,[1,2,3,4]).
Expected answer:

P = append([],[1,2,3,4]),
X = [],
Y = [1,2,3,4] ?
— and on backtracking gives all the splittings, because the predicate passed in is still the relational append/3.

maplist and reversibility

library(hiordlib) supplies the usual higher-order predicates, maplist among them:

?- maplist(plus2, [1,2,3], [4,5,6], R).
Expected answer:

R = [5,7,9] ?
Now the payoff for having written plus2/3 reversibly back on slide 15:

?- maplist(plus2, [1,2,3], L2, [5,7,9]).
Expected answer:

L2 = [4,5,6] ?
maplist ran backwards, because the predicate it was given runs backwards. Higher-order predicates inherit the reversibility of what you pass them — something a functional map cannot do, since a function only goes one way.

maplist with append/3 and shared variables gets quietly spectacular:

?- maplist(append, [ [a],   X,     X ],
                   [ [c,d], [e,f], Y ],
                   [ X,     Y,     Z] ).
Expected answer:

X = [a,c,d],
Y = [a,c,d,e,f],
Z = [a,c,d,a,c,d,e,f] ?
Three appends whose inputs and outputs are wired to each other — solved as a system of equations, not executed in the order written.

Slide 39 — Ciao's hiord package

With the hiord package you can write P(X,Y) directly, with the predicate in a variable, and get anonymous predicates as well.

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

:- use_module(library(lists)).
:- use_module(library(hiordlib)).

p(a,1).
p(b,2).
?- P = p, P(X,Y).
Expected answer:

P = p,
X = a,
Y = 1 ?
?- P = append, P([1,2],[3,4],Y).
Expected answer:

P = append,
Y = [1,2,3,4] ?
Anonymous predicates — read the _ as a lambda:

?- P = { _(X,Y) :- Y is X+1 }, P(3,R).
Expected answer:

P = {''(X,Y):-Y is X+1},
R = 4 ?
And they can be passed straight to maplist:

?- maplist({ _(X,Y) :- Y is X+1 }, [1,2,3], R).
Expected answer:

R = [2,3,4] ?
Remember that use_package(hiord) is per module, the top level included. To use this syntax at the prompt you need ?- use_package(hiord). there first.

Slides 40–42 — Aggregation predicates

findall/3, bagof/3 and setof/3 are meta-calls too: they run a goal repeatedly and collect the answers into a list. This is how you get from Prolog's one-answer-at-a-time backtracking to a data structure holding all the answers.

And it is meta-logical. You are stepping outside the logic and saying give me every solution in this proof tree — which is not something you can say within the logic. It needs extra machinery, and ISO Prolog provides three variants of it.

findall(Term, Goal, ListResults): ListResults is the list of all instances of Term for which Goal succeeds. If there are none, it is []. For it to terminate, the number of solutions must of course be finite and enumerable in finite time.

:- module(_,_).

likes(bill, cider).
likes(dick, beer).
likes(tom, beer).
likes(tom, cider).
likes(harry, beer).
likes(jan, cider).
?- findall(X, likes(X,Y), S).
Expected answer:

S = [bill,dick,tom,tom,harry,jan] ?
tom appears twice, because he satisfies the goal twice — findall/3 collects solutions, not distinct values.

The first argument is a pattern, and it does more than name a variable. It says what to build out of each solution:

?- findall(l(X), likes(X,Y), S).
Expected answer:

S = [l(bill),l(dick),l(tom),l(tom),l(harry),l(jan)] ?
Whatever term you write there is instantiated once per solution and collected. That is what makes these predicates as useful as they are.

And an empty result is an empty list, not a failure:

?- findall(X, likes(X,water), S).
Expected answer:

S = [] ?
findall/3 never fails. Remember it; it is the single most common surprise with this predicate.

The other thing to remember: findall/3 tries to get all the solutions. Call it on a goal with infinitely many — findall(N, nat(N), Ns) — and it does not come back. Eventually you run out of memory. That is the first consideration when using any of these predicates.

The set versions: setof and bagof

setof(Term, Goal, ListResults) gives the ordered set — sorted, no duplicates. Three differences from findall/3 matter:

  • If there are no instances, setof/3 fails (rather than giving []).
  • If Goal contains variables that do not appear in Term, the call can backtrack, producing one list per binding of those free variables.
  • A free variable can be existentially quantified with Var^Goal, which makes it behave like findall/3 in that respect.
bagof/3 is the same as setof/3 but leaves the list unsorted and keeps duplicates, in backtracking order.

Why three of them? Because they came from different Prolog systems. Everyone had the idea of an aggregation predicate and everyone made slightly different choices — and it turned out, after a while, that all the variants were useful. So the standard kept them all.

The ordering setof/3 uses is the standard order of terms, the same @< from slide 26 — which is what makes it able to sort any answers, not just numbers or atoms.

The backtracking behaviour is the interesting one, and it is the part people find strange at first.

Think about what you are asking for in setof(X, likes(X,Y), S). Y appears in the goal but not in the pattern, so it is a free variable — and it is genuinely unclear what you want. All the X for every Y lumped together? Or all the X for each Y separately?

findall/3 is simple: it is like calling Prolog and writing down every answer. setof/3 is sophisticated: for a free variable it gives you the set of solutions for each value of that variable, backtracking over them.

So setof/3 answers once per drink:

?- setof(X, likes(X,Y), S).

S = [dick,harry,tom],
Y = beer ? ;
S = [bill,jan,tom],
Y = cider ? ;
no
That is a group-by, obtained for free. Wrap it in another aggregation and you get the whole grouping in one term:

?- findall(t(Y,S), setof(X,likes(X,Y),S), Pairs).
Expected answer:

Pairs = [t(beer,[dick,harry,tom]),t(cider,[bill,jan,tom])] ?
If that is not what you wanted, say so: Y^Goal is existential quantification, read "there exists a Y such that …". Quantify Y away and you get everybody, once each:

?- setof(X, Y^likes(X,Y), S).
Expected answer:

S = [bill,dick,harry,jan,tom] ?
Note that tom is not repeated, even though he appears twice in the database — setof/3 always sorts and removes duplicates.

Rule of thumb. Put existential quantifiers on every variable that does not appear in the pattern and setof/3 behaves like findall/3, only sorted and duplicate-free. Leave them out and it does the backtracking thing. bagof/3 backtracks the same way but neither sorts nor removes duplicates.

And with no solutions at all, setof/3 fails where findall/3 returned []:

?- setof(X, likes(X,water), S).
Expected answer:

no
A small, pretty application — the powerset of a set, as a one-liner over a non-deterministic subset/2:

:- module(_,_).

subset([],[]).
subset([X|Xs],[X|Ys]) :- subset(Xs,Ys).
subset([_|Xs],Ys)     :- subset(Xs,Ys).

powerset(Set,Pset) :- setof(X,subset(Set,X),Pset).
?- powerset([1,2,3],Pset).
Expected answer:

Pset = [[],[1],[1,2],[1,2,3],[1,3],[2],[2,3],[3]] ?
subset/2 generates the subsets one at a time on backtracking; setof/3 gathers and sorts them. Eight of them, as it should be.

Exercises

Exercises on the aggregation predicates — opens in the Ciao playground, the same link as the button on the slide.

Slides 43–44 — Negation as failure

Negation is not a built-in primitive: it is defined, out of the meta-call, the cut, and fail/0.

:- module(_,_).

not( Goal) :- call(Goal), !, fail.
not(_Goal).

unmarried_student(X) :-
    \+ married(X),
    student(X).

student(joe).
married(john).
Read the two clauses. If Goal succeeds, we execute the cut — killing the second clause — and then fail: so not(Goal) fails, and because the cut removed every alternative before it, there is nowhere left to backtrack to. If Goal fails, the first clause fails before the cut, and the second clause succeeds unconditionally. Negation, in two lines, out of call/1, ! and fail/0.

The standard spelling is the prefix operator \+/1, so you write \+ member(c,[a,k,l]).

?- X = 1, \+ X = 2.
Expected answer:

X = 1 ?
?- \+ X = 1.
Expected answer:

no
no — because a free X can be unified with 1, so the goal being negated succeeds.

\+/1 never instantiates the variables of its goal — it cannot, since it only succeeds when the goal failed, and a failed goal leaves nothing behind. That makes the double negation \+ \+ Goal a genuinely useful idiom: it tests whether a goal is satisfiable while discarding all its bindings.

Here is the difference, on a concrete question — does this list start with 1?

:- module(_,_).

:- use_module(library(lists)).
?- L = [X,2,3], append([1],R,L).
Expected answer:

L = [1,2,3],
R = [2,3],
X = 1 ?
It answered yes — and changed the list: X got bound to 1. Now with the double negation:

?- L = [X,2,3], \+ \+ append([1],R,L).
Expected answer:

L = [X,2,3] ?
Same answer to the question, and L is untouched. Note also that the cut inside \+/1 means you get one yes-or-no answer and no choice points, where the direct call would have left them.

Termination. \+ Goal terminates exactly when the search for Goal reaches a success node before an infinite branch. If Goal loops, so does its negation.

And now the danger. With the program above:

?- unmarried_student(joe).
Expected answer:

yes
?- unmarried_student(X).
Expected answer:

no
Joe is an unmarried student — the first query says so. The second says no unmarried student exists. Both cannot be right.

What happened: \+ married(X) is called with X still unbound. It asks "is there any X such that married(X)?" — and there is, john — so married(X) succeeds, and the negation fails. The whole conjunction dies before student(X) is ever reached.

The deeper reason is the one above: negation does not instantiate. Even conceptually there is nothing for it to return — it does not know of any X for which the negation holds, only that the negation does not hold in general.

Asked about specific people it behaves perfectly: unmarried_student(joe) is yes, and unmarried_student(john) is no — for two independent reasons, since John is married and is not a student in this database. It is only generation that breaks.

Negation as failure is only correct for ground goals, and it is the programmer's responsibility to ensure that. The most frequent fix is simply to put the generator first, so the variable is bound by the time the negation runs:

unmarried_student(X) :- student(X), \+ married(X).
Now student(X) generates joe, and only then is married(joe) negated. unmarried_student(X) answers X = joe, and nobody else. Generate, then test — the same lesson as the goal-ordering slides in the previous deck, with sharper consequences.

Making the discipline explicit

You can check the condition rather than hoping for it:

not_(G) :-
    ground(G), !,
    \+ G.
not_(G) :-
    write('ERROR: Non-ground goal in negation: '), write(G), nl,
    abort.
Called through this, unmarried_student_with_check(X) reports ERROR: Non-ground goal in negation: married(_1223) and aborts, instead of quietly answering no.

Or state it as an assertion and let the system check it:

:- use_package([assertions,modes,rtchecks]).

:- pred not__(G) : ground(G).
not__(G) :- \+ G.
The : field is the call condition: G must be ground when called. With run-time checking on, a violation is reported automatically; the same assertion can also be verified statically.

One more meta-call plus cut, and a very common one:

once(G) :- call(G), !.
once/1 gives the first solution of G and no more. ?- once(member(X,[1,2,3])). answers X = 1 and offers no alternatives.

Exercises

Exercises on negation as failure — opens in the Ciao playground, the same link as the button on the slide.

Slide 45 — Cut-fail

The call, !, fail pattern in not/1 is worth naming on its own: a cut-fail combination forces a predicate to fail, which is how you say "no" in a language that has no way of asserting a negative.

The example is ground/1 itself — a term is ground if it contains no variables, so we fail as soon as we find one:

:- module(_,_).

ground_(Term):- var(Term), !, fail.
ground_(Term):-
    nonvar(Term),
    functor(Term,_F,N),
    ground_args(N,Term).

ground_args(0,_T).        %% all subterms traversed
ground_args(N,T):-
    N>0,
    arg(N,T,Arg),
    ground_(Arg),
    N1 is N-1,
    ground_args(N1,T).
The slide writes both predicates as ground, at arities 1 and 2. That is idiomatic Prolog — the two are different predicates — but Ciao warns when the clauses for one arity are separated from those of another, so the helper is called ground_args/2 here.

?- ground_(f(a,g(b))).
Expected answer:

yes
?- ground_(f(a,g(X))).
Expected answer:

no
The first clause is the cut-fail: if the term is a variable, cut away the second clause and fail — committing to "no" rather than letting the structure walk continue. The rest is the same descent through functor/3 and arg/3 as in subterm/2.

Useful, and dangerous for the same reason all cuts are: the failure is committed, so there is no backtracking into an alternative reading.

Dynamic program modification and meta-interpreters

Slide 46 — Modifying the program at run time

assert/1, retract/1, abolish/1 and friends modify the program while it is running. They are very powerful — you can also use them to simulate global variables.

Sometimes this is very useful. Very often it is a mistake. Code that modifies itself is hard to read, hard to understand, hard to debug — and it is typically slow. Use program modification sparingly, carefully, and locally.

There are cases where assertion and retraction can be logically justified:

  • asserting clauses that logically follow from the program — lemmas;
  • retracting clauses that are logically redundant.
Another usually harmless use is simple global switches.

Requirements differ between implementations. Typically a predicate that does not already appear in the program can always be asserted; otherwise it must be declared :- dynamic.

Slide 47 — Assert and retract in action

:- module(_,_).

related(3,4).
:- dynamic related/2.

relate_numbers(X,Y)   :- assert(related(X,Y)).
unrelate_numbers(X,Y) :- retract(related(X,Y)).
?- related(1,2).
Expected answer:

no
?- relate_numbers(1,2), related(1,2).
Expected answer:

yes
?- relate_numbers(1,2), unrelate_numbers(1,2), related(1,2).
Expected answer:

no
And abolish(related/2) removes the predicate altogether — after which calling it is an existence error, not a failure, because the predicate no longer exists.

Rules can be asserted too, not just facts. Since a clause is a term, you build the term and assert it:

:- module(_,_).

:- dynamic app/3.

define_app :-
    assert(   app([], X, X) ),
    assert( ( app([X|Y], Z, [X|W]) :- app(Y,Z,W) ) ).

erase_app :-
    abolish(app/3).
?- define_app, app([1,2],[3,4],Y).
Expected answer:

Y = [1,2,3,4] ?
A complete append/3, written into the program at run time — and note the extra parentheses around the rule, needed because :- has priority 1200, well above the comma's 1000.

You can do the same thing interactively, which makes the mechanism very concrete. A clause is a term whose functor is :-/2, so build it and hand it over:

?- C = (related(X,Y) :- X =< Y), assert(C).

C = (related(X,Y):-X=<Y) ?

?- related(1,2).     yes
?- related(2,1).     no
And now the consequence that matters: if you read/1 a term from a file and assert/1 it, that term is immediately executable. You can load a program from inside your program and run it, with no compiler, no linker, and no code of your own beyond those two calls.

retractall/1 removes every clause of a dynamic predicate at once, which is the usual way of starting over.

Slide 48 — Lemmas: the one clearly good use

Fibonacci is the standard illustration, because the naive definition recomputes the same values an exponential number of times.

:- module(_,_).

% 1) Direct definition:

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.

% 2) Version that records 'lemmas' (things already proved):

:- dynamic lemma_fib/2.
lemma_fib(0, 0).
lemma_fib(1, 1).

lfib(N, F) :-
    lemma_fib(N, F),          % already known? take it
    !.
lfib(N, F) :-
    N > 1,
    N1 is N - 1,
    N2 is N - 2,
    lfib(N1, F1),
    lfib(N2, F2),
    F is F1 + F2,
    assert(lemma_fib(N, F)).  % ...and remember it
You can already see why the first version is bad: computing fib(N-1) and fib(N-2) separately throws away all the work the first one did, because fib(N-1) computed fib(N-2) along the way.

Print a line on every call and ask for position 4 and you see it: the calls are 3, 2, 1, 0, 1, 2, 1, 0 — position 3 computed once, position 2 twice, position 1 three times. Exponential: each new number recomputes the whole tree beneath it.

The lemma version differs in exactly two places — look it up if we already have it, and record what we just computed. The first time you call it, it makes about the same number of calls, in fact slightly fewer, because as soon as it reaches a position it has already computed it stops descending. And the second call is where it pays: asking for position 5 after position 4 needs only one addition, because 4 and 3 are both already known.

What that buys, measured in this Ciao on the machine these notes were written on:

?- statistics(walltime,_), fib(27,Y), statistics(walltime,[_,T]).

T = 36.843,
Y = 196418 ?

?- statistics(walltime,_), lfib(27,Y), statistics(walltime,[_,T]).

T = 0.028,
Y = 196418 ?
Thirty-seven milliseconds against twenty-eight microseconds — three orders of magnitude, and the gap widens with N. And lfib/2 goes far beyond where the naive one can follow:

?- lfib(200,Y).
Expected answer:

Y = 280571172992510140037611932413038677189525 ?
This is the justified use. Every asserted lemma_fib(N,F) is a logical consequence of the program, so the meaning of the program does not change — only its running time. That is what makes lemmas different from the usual assert-based programming.

The slide's closing quotation is the right one: those who cannot remember the past are condemned to repeat it.

What you save in time you spend in memory — every lemma is stored.

And there is a practical trap. The lemmas survive: if you experiment at the top level and start getting results you cannot explain, remember that a previous run's assertions may still be there. Reload the module, or retractall(lemma_fib(_,_)) and re-assert the base cases, before you time anything.

Exercises

Exercise: measuring the effect of lemmas — opens in the Ciao playground, the same link as the button on the slide.

Slide 49 — Meta-interpreters

clause(Head, Body) reads a clause out of the program; for a fact, Body is true. The predicate must be declared dynamic for clause/2 to see it.

You cannot call it with the head completely unbound — that would be asking to enumerate every clause in the program — so you give it a head pattern and it enumerates the matching clauses on backtracking. For lappend/3 it returns the two you would expect:

cl(lappend([],A,A), true)
cl(lappend([B|C],A,[B|D]), lappend(C,A,D))
And here is the point that makes a meta-interpreter possible, and that is easy to miss: clause/2 unifies the head. The variables in the pattern you passed in come back bound by that unification.

But it does not execute anything. The difference shows up on rules: if you assert the fact related(0,0) then clause(related(X,Y),B) gives you X = 0, Y = 0; if you assert the rule related(X,Y) :- X = 0, Y = 0 — the same meaning — then clause/2 gives you back the body unexecuted, and X and Y stay free. For a fact, unifying and executing happen to be the same thing; for a rule they are not.

With it you can write an interpreter for Prolog, in Prolog, in three lines:

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

:- use_package(dynamic_clauses).

:- dynamic lappend/3.

lappend([],X,X).
lappend([X|Y],Z,[X|W]) :- lappend(Y,Z,W).

% The 'vanilla' meta-interpreter:
solve( true  ) :- !.
solve( (A,B) ) :- !, solve(A), solve(B).
solve( A     ) :- clause(A,B), solve(B).
Read the three clauses: true is solved, so we are finished; a pair (A,B) is solved by solving A and then B — and it is a pair because that is how clause bodies are built, conjunctions nested to the right with ,/2; and anything else is solved by finding a clause whose head it matches and solving that clause's body.

And because clause/2 performs the head unification for us, that third clause is doing exactly what the Prolog interpreter does. Parameter passing, clause selection, and the recursive descent are all there, in one line.

?- solve(lappend([1,2],[3,4],L)).
Expected answer:

L = [1,2,3,4] ?
This code also implements backtracking — and nowhere does it say so. clause/2 leaves a choice point, because A may unify with several clause heads, and Prolog's own backtracking then does the rest. Run it in the other direction and all five splittings come back:

?- solve(lappend(X,Y,[1,2,3,4])).
Expected answer:

X = [],
Y = [1,2,3,4] ? ;
X = [1],
Y = [2,3,4] ? ;
X = [1,2],
Y = [3,4] ? ;
X = [1,2,3],
Y = [4] ? ;
X = [1,2,3,4],
Y = [] ? ;
no

Two practical points in Ciao. The clauses must be dynamic for clause/2 to reach them, and the dynamic_clauses package is the version of dynamic that makes them visible to clause/2. Also, the meta-interpreter can only see clauses in its own module — which is the module system doing its job.

Slide 50 — Extending the meta-interpreter

The three lines are a skeleton, and the point is how easily it is extended: tracing, debugging, explanation facilities for expert systems, alternative computation rules.

Tracing is the easiest. Put a write/1 and nl/0 in the third clause and every goal the interpreter attempts is printed — without modifying the program being interpreted at all. Put them after the recursive call instead and you see the state on the way back out, which is how the answer list gets built. That is already a usable debugger, in two extra goals.

Add one argument instead and you have a step counter:

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

:- use_package(dynamic_clauses).

:- dynamic lappend/3.

lappend([],X,X).
lappend([X|Y],Z,[X|W]) :- lappend(Y,Z,W).

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.
?- csolve(lappend([1,2],[3,4],L),N).
Expected answer:

L = [1,2,3,4],
N = 3 ?
Three steps. Now the question the example file asks: what does the cost of append/3 depend on?

?- csolve(lappend([1,2,a,b],[3,4],L),N).
Expected answer:

L = [1,2,a,b,3,4],
N = 5 ?
?- csolve(lappend([1,2],[3,4,a,b],L),N).
Expected answer:

L = [1,2,3,4,a,b],
N = 3 ?
Lengthening the first list costs steps; lengthening the second costs nothing. append/3 recurses on its first argument only, and the cost is the length of that list plus one — measured here, not argued for.

Trace the three steps against the program and they line up exactly: one for each element of [1,2], and one more where the base case lappend([],X,X) matches. This is a genuinely useful way to find the complexity of a predicate: you get a number rather than an argument.

Exercises

Exercise: extending the meta-interpreter — opens in the Ciao playground, the same link as the button on the slide.

Incomplete data structures

Slide 51 — Difference lists

This is the place where you see most clearly that logical variables are pointers — declarative pointers — and it is very satisfying.

Start with why ordinary append/3 is expensive. You have a pointer X to [1,2,3], ending in [], and a pointer Y to [4,5]. append(X,Y,Z) builds the result by taking the head off the first list and putting it at the front of the result, over and over — that is, it copies the first list. Only when it reaches the base case, append([],L,L), does it make the last cell point at Y. So the second list is not copied; the first one is, entirely. That is where the cost comes from.

What you would rather do is attach something at the end of the first list. And you can — if you leave the tail as a free variable and keep a pointer to it. Then appending is a single unification: point the tail at the second list. Constant time.

[1,2,3] is a closed list: it ends in [], and you cannot add to the end without walking to it first — which is exactly why append/3 costs the length of its first argument.

An incomplete (or open) list ends in a variable: [1,2,3|T]. Now you can add at the end, by instantiating T. The idea of a difference list is to carry that variable around with the list, so you always have a pointer to the end:

:- module(_,_).

% A pseudo-type for difference lists:
dlist(X-X).
dlist([_|DL]-X) :- dlist(DL-X).

% Appending difference lists, in constant time:
append_dl(B1-E1, E1-E2, B1-E2).
The -/2 is just a functor pairing the list with its own tail; nothing arithmetic is going on. It is traditional, and it is where the name "difference list" comes from — you are carrying the list together with what must be subtracted from it.

What is the empty difference list? A three-element one is [1,2,3|T]-T. The empty one is the pair whose two halves are the same variable: X-X, the head pointing at the tail with nothing in between.

Which is what the pseudo-type says:

dlist(X-Y)      :- var(X), !, X == Y.
dlist([_|DL]-X) :- dlist(DL-X).
Note the ! and the ==/2 — this is not pure; it is a Prolog program, and it could not have been written in the previous deck.

The explicit version of the append reads more clearly:

append_dl(B1-E1,B2-E2,B3-E3) :- B3=B1, E3=E2, B2=E1.
The result starts where the first list starts, ends where the second ends, and the second list is plugged into the hole at the end of the first. Written compactly, all three unifications happen in the head — which is why append_dl/3 is a fact, and runs in constant time regardless of length.

Note the slide's aside, and take it seriously: people who work with difference lists never write append_dl/3. It is a toy for understanding what is going on. In real code you do the binding on the fly, wherever you happen to be — as the quicksort below does.

And the same trick generalizes. These are really incomplete data structures, not merely incomplete lists: leave variables at the ends of any structure and you can extend it there. Difference trees have variables at the leaves, and from them you get dictionaries and queues. It is one of the great tricks of logic programming — and specifically of Prolog.

Slide 52 — Playing with difference lists

First by hand, with no append_dl/3 at all — just unify the second list with the first one's tail:

?- L1 = [1,2,3|X], L2 = [4,5|Y], L2 = X.
Expected answer:

L1 = [1,2,3,4,5|Y],
L2 = [4,5|Y],
X = [4,5|Y] ?
L1 is now [1,2,3,4,5|Y], and one unification did it.

Now through the predicate:

?- append_dl([1,2,3|X]-X, [4,5|Y]-Y, L).
Expected answer:

L = [1,2,3,4,5|Y]-Y,
X = [4,5|Y] ?
Look at what happened to X: it is no longer free. We have modified the first list, and we cannot append to it again — the hole has been filled. That is the price of constant-time append: each difference list can be extended once.

And it runs backwards, because it is a fact and everything is unification. Given the result and the second list, it recovers the first:

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

L = [1,2,3,4,5|Y],
X = [4,5|Y],
Z = Y ?

Slides 53–54 — Quicksort with ordinary lists

The textbook version, which needs an append/3 at the end of every recursive step:

:- module(_,_).

:- use_module(library(lists)).

qsort([],[]).
qsort([X|L],S) :-           % take the first element of the list in X
   partition(L,X,LS,LB),    % LS: elements of L < X;  LB: elements >= X
   qsort(LS,LSS),           % LSS is LS sorted
   qsort(LB,LBS),           % LBS is LB sorted
   append(LSS,[X|LBS],S).   % put the pivot between the two sorted halves

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).
?- qsort([5,2,1,3,7,6], SL).
Expected answer:

SL = [1,2,3,5,6,7] ?
Trace the top call: X = 5, L = [2,1,3,7,6]; partition gives LS = [2,1,3] and LB = [7,6]; sorting those gives [1,2,3] and [6,7]; and the final call is append([1,2,3],[5,6,7],S).

That append/3 is the waste. It walks the whole left half again, at every level of the recursion.

In Prolog, seeing an append/3 is usually a bad sign. It means the data structures have not been thought through — that you are not thinking about pointers. In this language you have to think about the logic and about the pointers and the efficiency at the same time.

Which argument should be a difference list?

Before rewriting, decide what needs to change, and the reasoning is short.

Consider the input list first. We have to walk down it anyway — there is no way to sort a set without looking at every element — and we never append to it. So it can stay an ordinary list.

Now the result. That is where the appends are, because we have the small ones and the big ones and have to join them. So that is what becomes a difference list.

The first argument of qsort_dl_ is a normal list; the second is a difference list. That one sentence is the design.

And a pleasant consequence: partition/4 does not change at all. It takes a normal list and a pivot and produces two normal lists — which is exactly what the recursive calls want in their first argument.

Slides 55–57 — Quicksort with difference lists, in three steps

Version 1 — with the -/2 functor and every unification written out, so you can see what is being connected to what:

:- module(_,_).

qsort_dl1(L,SL) :-          % L  = [5,2,1,3,7,6]
    qsort_dl1_(L,SL-SLE),   % SL = [1,2,3,5,6,7|SLE]
    SLE = [].               % SL = [1,2,3,5,6,7]

qsort_dl1_([],SLE-SLE).
qsort_dl1_([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_dl1_(S,SS-SSE),   % SS  = [1,2,3|SSE]
    qsort_dl1_(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]

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).
?- qsort_dl1([5,2,1,3,7,6], SL).
Expected answer:

SL = [1,2,3,5,6,7] ?
Reason about it recursively, which means believing the recursion works. We give it [5,2,1,3,7,6]; the pivot is 5, and partition/4 gives S = [2,1,3] and B = [7,6] as ordinary lists. Sort those: the first call returns [1,2,3] as a difference list, SS to SSE; the second returns [6,7], BS to BSE.

Now comes the point where the old version did the append/3 — and here is the magic. What came back are difference lists, so we have a handle on the end of each. Three unifications suffice:

  • SL = SS — the result starts where the small ones start.
  • SLE = BSE — the result ends where the big ones end.
  • SSE = [X|BS] — and the hole at the end of the small ones gets the pivot followed by the big ones.
Follow the pointers: SL runs through [1,2,3], hits SSE, which is now [5|BS], runs through [6,7], and ends at BSE, which is SLE. [1,2,3,5,6,7], assembled by binding three variables.

No traversal, no append/3. And the top-level qsort_dl1/2 then does one last thing for the benefit of a caller who knows nothing about difference lists: SLE = [] closes the open tail, converting the difference list back into an ordinary one.

Version 2 — the same program, with the unifications moved into the head and the arguments, where they belong:

:- module(_,_).

qsort_dl2(L,SL) :-
    qsort_dl2_(L,SL-[]).

qsort_dl2_([],SLE-SLE).
qsort_dl2_([X|L],SL-SLE) :-
    partition(L,X,S,B),
    qsort_dl2_(S,SL-[X|BS]),
    qsort_dl2_(B,BS-SLE).

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).
?- qsort_dl2([5,2,1,3,7,6], SL).
Expected answer:

SL = [1,2,3,5,6,7] ?
Six lines of explicit =/2 have vanished into two argument positions. qsort_dl2_(S,SL-[X|BS]) says in one term: sort S into the front of SL, and let its tail be the pivot followed by whatever BS turns out to be.

Version 3 — drop the -/2 functor and use two arguments instead:

:- 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).

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).
?- qsort_dl([5,2,1,3,7,6], SL).
Expected answer:

SL = [1,2,3,5,6,7] ?
The -/2 was only ever notation; two plain arguments carry the same pair, and one term construction per call is saved. In fact the - version is slightly more expensive, since it builds a structure that nothing needs.

Remember this shape — a pair of arguments, one for the list and one for its tail — because it is exactly what the grammar section is about.

Exercises

Exercises on difference lists — opens in the Ciao playground, the same link as the button on the slide.

Parsing and grammars

The goal of this section is to parse the plane flies — and to arrive at a notation in which the program is the grammar. We get there in four steps, each one a mechanical improvement on the last.

Slide 58 — Parsing with append and ordinary lists

The obvious way: chop the input into pieces with append/3 and check each piece.

:- module(_,_).

:- use_module(library(lists)).

myphrase(P) :-
    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([' ' | Y]) :- spaces(Y).

noun([c,a,r]).
noun([p,l,a,n,e]).

verb([f,l,i,e,s]).
verb([d,r,i,v,e,s]).
?- myphrase([t,h,e,' ',p,l,a,n,e,' ',f,l,i,e,s]).
Expected answer:

yes
A grammar is a description of the syntax of the phrases we want to recognize — formally similar to the automata of the previous deck, but richer and higher-level. Words are represented as lists of letters: a is [a], "the" is [t,h,e]. Spaces are one or more, which is an ordinary two-clause recursion.

How does it work? article(A) picks an article — a or the — and then append(A,T1,P), run backwards, asks whether the phrase starts with it, handing back the rest as T1. Then the same question is asked of T1, and so on down the phrase. append/3 is being used to subtract a piece off the front.

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

no
A leading space is rejected — nothing in the grammar allows one. But extra spaces inside are fine, because spaces/1 is recursive:

?- myphrase([a,' ',' ',p,l,a,n,e,' ',d,r,i,v,e,s]).
Expected answer:

yes
Watch the backtracking, because it is the grammar working. Parsing a plane drives: article/1 offers a, which matches the front. spaces/1 offers one space, which matches. noun/1 offers car — and append/3 fails, since the phrase does not continue with car. Back to the choice point in noun/1, which offers plane; that matches. And so on. When two spaces appear where the grammar first tried one, spaces/1 is re-entered and offers two.

Every alternative in the grammar is a clause, and every backtrack is Prolog doing what it does anyway.

It works, and it is generate and test: append/3 proposes every possible split of the list, and article/1 and the rest reject the bad ones. Every split of every remaining tail — the cost is appalling, and there are four append/3 calls.

Slide 59 — The same, with difference lists

Replace each list by a pair: the list, and what is left of it after this piece has been consumed. Then no splitting is needed at all — each non-terminal takes what it wants off the front and hands the rest on.

:- module(_,_).

myphrase(P,C) :-
    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],[]).
Expected answer:

yes
Every append/3 is gone. article([t,h,e|T],T) says if the input starts with the, the remainder is T — a fact, matched by head unification alone, in constant time. The [] in the query says "and there must be nothing left over".

This is the difference-list pair from the quicksort section, doing a completely different job. The chaining of the variables — P to CA to CS1 to CN to CS2 to C — is the sequencing of the grammar.

Slide 60 — The same again, with string syntax

Writing [t,h,e|T] is unbearable. Strings are lists of character codes, so "the" is [0't,0'h,0'e], that is [116,104,101] — and Ciao's || operator does for strings what | does for lists:

:- module(_,_).

myphrase(X,C) :-
    article(X,CA), spaces(CA,CS1),
    noun(CS1,CN),  spaces(CN,CS2),
    verb(CS2,C).

article("a"     || T, T).
article("the"   || 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",[]).
Expected answer:

yes
?- myphrase("the plane drove",[]).
Expected answer:

no
Nothing changed but the notation. "the" || T is exactly [t,h,e|T] with the characters as codes.

Slides 61–62 — Definite clause grammars

The last irritation is the chain of variables X, CA, CS1, CN, CS2, C. Look at what they do: the output argument of each non-terminal is the input argument of the next, all the way down, and the last one is the output of the whole thing. It is completely mechanical — you would write the same threading in every grammar you ever wrote.

So let the system add them. That transformation is what a definite clause grammar is: --> is a macro that expands into the threaded two-argument version.

:- module(_,_).

:- use_package(dcg).

myphrase --> article, spaces, noun, spaces, verb.

article --> "a".
article --> "the".

spaces  --> " ".
spaces  --> " ", spaces.

noun    --> "car".
noun    --> "plane".

verb    --> "flies".
verb    --> "drives".
That is a grammar. Read it aloud: a phrase is an article, spaces, a noun, spaces and a verb. The --> clauses are translated into exactly the two-argument predicates of the previous slide — myphrase --> article, spaces, ... becomes

myphrase(X,CV) :- article(X,CA), spaces(CA,CS1), noun(CS1,CN),
                  spaces(CN,CS2), verb(CS2,CV).
and article --> "a". becomes article("a" || X, X). Nothing new is happening; the arguments are merely implicit.

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

yes
Note that the query passes two arguments — because what is really being called is the expanded version; the arrow put them there.

This is a real parser. You load the dcg package and you can write grammars: for your data files, for languages you design, for compilers, for HTML, for whatever you like. You never have to reach for yacc or lex.

You can also use the phrase/2 built-in, which supplies the [] for you:

?- phrase(myphrase,"the plane flies").
Expected answer:

yes
And because this is still just a logic program, the grammar also generates:

?- myphrase(S,[]).
Expected answer:

S = "a car flies" ?
The same six lines that recognize sentences also produce them, on backtracking. A parser generator in another language gives you a recognizer; here you get the relation.

(Under depth-first search the enumeration will get stuck in the recursive spaces rule. Add :- use_package(sr/bfall). and ask again to see the whole language come out.)

Slide 63 — Parsing with actions

What else would you want to do while parsing a program? Run it. Generate machine code. Compile it. People have rediscovered this idea and called it attributed grammars — a grammar with actions attached. In Prolog you do not have to invent it; it is already there.

Grammar rules can carry extra arguments, and can call ordinary Prolog between braces { ... } — anything in braces is straight Prolog and does not get the hidden arguments added. And note where the extra arguments go: the two hidden ones are always the last two, so yours go in front of them.

Here is the same grammar, counting the characters it consumes:

:- module(_,_).

:- use_package(dcg).

myphrase(N) --> article(AC), spaces(S1), noun(NC), spaces(S2),
                verb(VC), { 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".
?- myphrase(N,"the plane flies",[]).
Expected answer:

N = 15 ?
?- phrase(myphrase(N),"the plane flies").
Expected answer:

N = 15 ?
Fifteen characters: 3 + 1 + 5 + 1 + 5.

Read the rules. article(1) --> "a". says an article may be a, and it has one character. The spaces rule is the pretty one: one space has one character; and a space followed by more spaces, which have N1 characters, has N1 + 1 — with { N is N1+1 } being plain Prolog dropped into the middle of a grammar rule. So spaces//1 really has three arguments: the two hidden ones doing the difference-list threading, and the extra one carrying the count.

The counts travel in that extra argument, and a final { ... } adds them up. Which is exactly how you would build an abstract syntax tree as you parse, or compute a value, or check a context condition — the grammar and the semantic actions live in the same notation.

DCGs are enormously useful. Learning to use them will simplify your life; the grammars alone would justify the language.

Exercises

Exercises on DCGs — opens in the Ciao playground, the same link as the button on the slide.

What is left, and where to look

Slide 64 — Other issues in Prolog

Things this part of the course does not cover, but which you will meet. "The Art of Prolog" and the bibliography are the places to go.

Repeat loops and failure-driven loops. The pattern is worth recognizing, because it is what imperative Prolog looks like:

main(_) :- repeat, read(X), process(X).

process(end).
process(X) :- display(X), nl, fail.
repeat/0 succeeds infinitely often on backtracking, and fail/0 drives the loop round again — so this reads a term, prints it, fails back into repeat, and reads the next, until end arrives and the first clause of process/1 stops the loop. It is a while loop built out of backtracking.

Also: exception handling; extending the syntax beyond operators with term expansions and macros, which is what a package is; delay declarations and concurrency; the operating system interface, sockets; foreign language interfaces such as C; and a great many more built-ins.

Slide 65 — Libraries

Most systems come with a substantial library, and it is worth looking before you reimplement. A representative sample of what is normally there:

Arrays Assoc Attributes Heaps
Lists Term Utilities Ordset Queues
Random System Utilities Tree UGraphs
WGraphs Sockets Linda/Distribution Persistent DB
CLPB CLPQR CLPFD Objects
GCLA TclTk Tracing Chars I/O
Runtime Utilities Timeout Xrefs WWW
Java Interface ... ... ...

Slides 66–68 — Extensions, taking Ciao as the example

Other systems offer their own; these give an idea of the range.

Other execution rules. Breadth-first, iterative deepening, random; tabling; CASP (negation with multiple models); Andorra ("determinate-first") execution; fuzzy Prolog. You have used one of these all along — sr/bfall — so the mechanism is already familiar.

Interfaces to other languages and systems. C, Java, JavaScript, Python, LLVM; an SQL database interface and persistent predicates; web, HTML, XML and CGI programming (PiLLoW), HTTP connectivity, JSON, compilation to JavaScript; interfaces to solvers (PPL, Mathematica, MiniSAT, Z3, Yices); Graphviz and daVinci; Electron, wxWidgets, Tcl/Tk, VRML.

Syntactic and semantic extensions. Functional notation and higher order — both of which appeared in these notes; terms with named arguments (records and feature terms); multiple argument indexing; the script interpreter; active modules for high-level distributed execution; concurrency and multithreading; attributed variables; object-oriented programming.

Constraint programming. Rationals, reals, finite domains; CHR (constraint handling rules), GeCode. This is the subject of the last part of the course, and it is where the arithmetic we lost on slide 13 comes back — declarative and efficient.

Assertions, which appeared here twice as a way of stating what a predicate expects: regular types, modes, determinacy and other properties; run-time checking; assertion-based unit tests and automatic test case generation; and compile-time property inference and assertion checking with CiaoPP.

Additional programming support. Automatic documentation (LPdoc — which produced the document you are reading); partial evaluation, optimization and parallelization (CiaoPP).

Looking back over the whole part: everything Prolog added to pure logic programming bought efficiency or expressive power, and the price was always the same one — the clause stopped being a statement you could read on its own. Arithmetic gave up reversibility; the type tests and the meta-logical predicates gave up order-independence; the cut gave up the declarative reading; assert gave up having a fixed program at all.

Sometimes that is exactly the right trade. The next part of the course shows that for arithmetic, at least, it was never necessary.