Download unit 8 - WordPress.com

Survey
yes no Was this document useful for you?
   Thank you for your participation!

* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project

Document related concepts

Common Lisp wikipedia , lookup

Falcon (programming language) wikipedia , lookup

C Sharp (programming language) wikipedia , lookup

Curry–Howard correspondence wikipedia , lookup

Anonymous function wikipedia , lookup

Closure (computer programming) wikipedia , lookup

Lambda calculus wikipedia , lookup

Currying wikipedia , lookup

Lambda lifting wikipedia , lookup

Lambda calculus definition wikipedia , lookup

Combinatory logic wikipedia , lookup

Standard ML wikipedia , lookup

Transcript
UNIT 8
Functional Languages
8.1: Functional Programming Concepts
8.2: A Review/Overview of Scheme
8.3: Evaluation Order Revisited
8.4: Higher-Order Functions
8.5: Theoretical Foundations
8.6: Logic Programming Concepts
8.7: Prolog
8.8: Theoretical Foundations
8.9: Logic Programming in Perspective
1
8.1 Functional Programming Concepts
In a strict sense of the term, functional programming defines the outputs of a program as
a mathematical function of the inputs, with no notion of internal state, and thus no side
effects. Among the languages we consider here, Miranda, Haskell, pH, Sisal, and Single
Assignment C are purely functional. Erlang is nearly so. Most others include imperative
features. To make functional programming practical, functional languages provide a
number of features that are often missing in imperative languages, including:

First-class function values and higher-order functions

Extensive polymorphism

List types and operators

Structured function returns

Constructors (aggregates) for structured objects

Garbage collection
A first-class value can be defined as one that can be passed as a parameter, returned from
a subroutine, or assigned into a variable. Under a strict interpretation of the term, firstclass status also requires the ability to create (compute) new values at run time. In the
case of subroutines, this notion of first-class status requires nested lambda expressions
that can capture values (with unlimited extent) defined in surrounding scopes.
Subroutines are second-class values in most imperative languages, but first-class values
(in the strict sense of the term) in all functional programming languages. A higher-order
function takes a function as an argument, or returns a function as a result.
Polymorphism is important in functional languages because it allows a function to be
used on as general a class of arguments as possible.Lisp and its dialects are dynamically
typed, and thus inherently polymorphic, while ML and its relatives obtain polymorphism
through the mechanism of type inference. Lists are important in functional languages
because they have a natural recursive definition, and are easily manipulated by operating
on their first element and (recursively) the remainder of the list. Recursion is important
because in the absence of side effects it provides the only means of doing anything
repeatedly.
Several of the items in our list of functional language features (recursion, structured
function returns, constructors, garbage collection) can be found in some but not all
imperative languages. Fortran 77 has no recursion, nor does it allow structured types (i.e.,
arrays) to be returned from functions. Pascal and early versions of Modula-2 allow only
simple and pointer types to be returned from functions. several imperative languages,
including Ada, C, and Fortran 90, provide aggregate constructs that allow a structured
value to be specified in-line
2
8.2 A Review/Overview of Scheme
The read-eval-print loop
Most Scheme implementations employ an interpreter that runs a "read-eval-print" loop.
The interpreter repeatedly reads an expression from standard input (generally typed by
the user), evaluates that expression, and prints the resulting value. If the user types
(+ 3 4)
the interpreter will print
7
If the user types
7
the interpreter will also print
7
(The number 7 is already fully evaluated.) To save the programmer the need to type an
entire program verbatim at the keyboard, most Scheme implementations provide a load
function that reads (and evaluates) input from a file:
(load "my_Scheme_program")
Significance of parentheses
Scheme (like all Lisp dialects) uses Cambridge Polish notation for expressions.
Parentheses indicate a function application (or in some cases the use of a macro). The
first expression inside the left parenthesis indicates the function; the remaining
expressions are its arguments. Suppose the user types
((+ 3 4))
When it sees the inner set of parentheses, the interpreter will call the function +, passing 3
and 4 as arguments. Because of the outer set of parentheses, it will then attempt to call 7
as a zero-argument function¡ªa run-time error:
eval: 7 is not a procedure
Unlike the situation in almost all other programming languages, extra parentheses change
the semantics of Lisp/Scheme programs.
(+ 3 4) ⇒ 7
((+ 3 4)) ⇒ error
3
Here the ⇒ means "evaluates to." This symbol is not a part of the syntax of Scheme
itself.
Quoting
One can prevent the Scheme interpreter from evaluating a parenthesized expression by
quoting it:
(quote (+ 3 4)) ⇒ (+ 3 4)
Here the result is a three-element list. More commonly, quoting is specified with a special
shorthand notation consisting of a leading single quote mark:
'(+ 3 4) ⇒ (+ 3 4)
Dynamic typing
Though every expression has a type in Scheme, that type is generally not determined
until run time. Most predefined functions check dynamically to make sure that their
arguments are of appropriate types. The expression
(if (> a 0) (+ 2 3) (+ 2 "foo"))
will evaluate to 5 if a is positive, but will produce a run-time type clash error if a is
negative or zero. functions that make sense for arguments of multiple types are implicitly
polymorphic:
(define min (lambda (a b) (if (< a b) a b)))
The expression (min 123 456) will evaluate to 123; (min 3.14159 2.71828) will evaluate
to 2.71828.
Type predicates
User-defined functions can implement their own type checks using predefined type
predicate functions:
(boolean? x) ; is x a Boolean?
(char? x)
; is x a character?
(string? x)
; is x a string?
(symbol? x)
; is x a symbol?
(number? x)
; is x a number?
(pair? x)
(list? x)
; is x a (not necessarily proper) pair?
; is x a (proper) list?
(This is not an exhaustive list.)
Liberal syntax for symbols
4
A symbol in Scheme is comparable to what other languages call an identifier. The lexical
rules for identifiers vary among Scheme implementations, but are in general much looser
than they are in other languages. In particular, identifiers are permitted to contain a wide
variety of punctuation marks:
(symbol? 'x$_%:&=*!) ⇒ #t
The symbol #t represents the Boolean value true. False is represented by #f. Note the use
here of quote ('); the symbol begins with x.
Lambda expressions
To create a function in Scheme one evaluates a lambda expression: [3]
(lambda (x) (* x x)) ⇒ function
The first "argument" to lambda is a list of formal parameters for the function (in this case
the single parameter x). The remaining "arguments" (again just one in this case)
constitute the body of the function. Scheme differentiates between functions and socalled special forms (lambda among them), which resemble functions but have special
evaluation rules. Strictly speaking, only functions have arguments, but we will also use
the term informally to refer to the subexpressions that look like arguments in a special
form.
Function evaluation
When a function is called, the language implementation restores the referencing
environment that was in effect when the lambda expression was evaluated (like all
languages with static scope and first-class, nested subroutines, Scheme employs deep
binding). It then augments this environment with bindings for the formal parameters and
evaluates the expressions of the function body in order. The value of the last such
expression (most often there is only one) becomes the value returned by the function:
((lambda (x) (* x x)) 3) ⇒ 9
If expressions
Simple conditional expressions can be written using if:
(if (< 2 3) 4 5) ⇒ 4
(if #f 2 3)
⇒3
In general, Scheme expressions are evaluated in applicative. Special forms such as
lambda and if are exceptions to this rule. The implementation of if checks to see whether
the first argument evaluates to #t. If so, it returns the value of the second argument,
without evaluating the third argument. Otherwise it returns the value of the third
argument, without evaluating the second.
Bindings
Nested scopes with let
Names can be bound to values by introducing a nested scope:
5
(let ((a 3)
(b 4)
(square (lambda (x) (* x x)))
(plus +))
(sqrt (plus (square a) (square b)))) ⇒ 5.0
The special form let takes two or more arguments. The first of these is a list of pairs. In
each pair, the first element is a name and the second is the value that the name is to
represent within the remaining arguments to let. Remaining arguments are then evaluated
in order; the value of the construct as a whole is the value of the final argument.
The scope of the bindings produced by let is let's second argument only:
(let ((a 3))
(let ((a 4)
(b a))
(+ a b))) ⇒ 7
Here b takes the value of the outer a. The way in which names become visible "all at
once" at the end of the declaration list precludes the definition of recursive functions. For
these one employs letrec:
(letrec ((fact
(lambda (n)
(if (= n 1) 1
(* n (fact (- n 1)))))))
(fact 5))
⇒ 120
There is also a let* construct in which names become visible "one at a time" so that later
ones can make use of earlier ones, but not vice versa.
Global bindings with define
Scheme is statically scoped. (Common Lisp is also statically scoped. Most other Lisp
dialects are dynamically scoped.) While let and letrec allow the user to create nested
scopes, they do not affect the meaning of global names (names known at the outermost
level of the Scheme interpreter). For these Scheme provides a special form called define
that has the side effect of creating a global binding for a name:
(define hypot
6
(lambda (a b)
(sqrt (+ (* a a) (* b b)))))
(hypot 3 4)
⇒5
Lists and Numbers
Basic list operations
Like all Lisp dialects, Scheme provides a wealth of functions to manipulate lists. We saw
many of these in Section 7.8; we do not repeat them all here. The three most important
are car, which returns the head of a list, cdr ("coulder"), which returns the rest of the list
(everything after the head), and cons, which joins a head to the rest of a list:
(car '(2 3 4)) ⇒ 2
(cdr '(2 3 4)) ⇒ (3 4)
(cons 2 '(3 4)) ⇒ (2 3 4)
Also useful is the null? predicate, which determines whether its argument is the empty
list. Recall that the notation '(2 3 4) indicates a proper list, in which the final element is
the empty list:
(cdr '(2)) ⇒ ()
(cons 2 3) ⇒ (2 . 3) ; an improper list
For fast access to arbitrary elements of a sequence, Scheme provides a vector type that is
indexed by integers, like an array, and may have elements of heterogeneous types, like a
record. Interested readers are referred to the Scheme manual for further information.
Scheme also provides a wealth of numeric and logical (Boolean) functions and special
forms. The language manual describes a hierarchy of five numeric types: integer,
rational, real, complex, and number. The last two levels are optional: implementations
may choose not to provide any numbers that are not real. Most but not all
implementations employ arbitrary-precision representations of both integers and
rationals, with the latter stored internally as (numerator, denominator) pairs.
Equality Testing and Searching
Scheme provides several different equality-testing functions. For numerical comparisons,
= performs type conversions where necessary (e.g., to compare an integer and a floatingpoint number). For general-purpose use, eqv? performs a shallow comparison, while
equal? performs a deep (recursive) comparison, using eqv? at the leaves. The eq?
function also performs a shallow comparison, and may be cheaper than eqv? in certain
circumstances (in particular, eq? is not required to detect the equality of discrete values
stored in different locations, though it may in some implementations).
7
List search functions
To search for elements in lists, Scheme provides two sets of functions, each of which has
variants corresponding to the three general-purpose equality predicates. The functions
memq, memv, and member take an element and a list as argument, and return the longest
suffix of the list (if any) beginning with the element:
(memq 'z '(x y z w))
(z w)
(memv '(z) '(x y (z) w)) #f
; (eq? '(z) '(z))
#f
(member '(z) '(x y (z) w)) ((z) w) ; (equal? '(z) '(z)) #t
The memq, memv, and member functions perform their comparisons using eq?, eqv?, and
equal?, respectively. They return #f if the desired element is not found. It turns out that
Scheme's conditional expressions (e.g., if) treat anything other than #f as true. [4] One
therefore often sees expressions of the form
(if (memq desired-element list-that-might-contain-it) ...
Searching association lists
Control Flow and Assignment
Multiway conditional expressions
We have already seen the special form if. It has a cousin named cond that resembles a
more general if¡ elsif¡ else:
(cond
((< 3 2) 1)
((< 4 3) 2)
(else 3))
3
The arguments to cond are pairs. They are considered in order from first to last. The
value of the overall expression is the value of the second element of the first pair in
which the first element evaluates to #t. If none of the first elements evaluates to #t, then
the overall value is #f. The symbol else is permitted only as the first element of the last
pair of the construct, where it serves as syntactic sugar for #t.
Assignment
For programmers who wish to make use of side effects, Scheme provides assignment,
sequencing, and iteration constructs. Assignment employs the special form set! and the
functions set-car! and set-cdr!:
(let ((x 2)
; initialize x to 2
8
(l '(a b)))
; initialize l to (a b)
(set! x 3)
; assign x the value 3
(set-car! l'(c d))
; assign head of l the value (c d)
(set-cdr! l'(e))
; assign rest of l the value (e)
... x
... l
3
((c d) e)
The return values of the various varieties of set! are implementation-dependent.
Sequencing
Sequencing uses the special form begin:
(begin
(display "hi ")
(display "mom"))
Iteration
Iteration uses the special form do and the function for-each:
(define iter-fib (lambda (n)
; print the first n+1 Fibonacci numbers
(do ((i 0 (+ i 1))
(a 0 b)
(b 1 (+ a b)))
((= i n) b)
(display b)
(display " "))))
; initially 0, inc'ed in each iteration
; initially 0, set to b in each iteration
; initially 1, set to sum of a and b
; termination test and final value
; body of loop
; body of loop
(for-each (lambda (a b) (display (* a b)) (newline))
'(2 4 6)
'(3 5 7))
The first argument to do is a list of triples, each of which specifies a new variable, an
initial value for that variable, and an expression to be evaluated and placed in a fresh
instance of the variable at the end of each iteration. The second argument to do is a pair
that specifies the termination condition and the expression to be returned. At the end of
each iteration all new values of loop variables (e.g., a and b) are computed using the
current values. Only after all new values are computed are the new variable instances
created.
9
The function for-each takes as argument a function and a sequence of lists. There must be
as many lists as the function takes arguments, and the lists must all be of the same length.
For-each calls its function argument repeatedly, passing successive sets of arguments
from the lists. In the example shown here, the unnamed function produced by the lambda
expression will be called on the arguments 2 and 3, 4 and 5, and 6 and 7. The interpreter
will print
6
20
42
()
The last line is the return value of for-each, assumed here to be the empty list. The
language definition allows this value to be implementation-dependent; the construct is
executed for its side effects.
8.3Evaluation Order Revisited
the subcomponents of many expressions can be evaluated in more than one order. In
particular, one can choose to evaluate function arguments before passing them to a
function, or to pass them unevaluated. The former option is called applicative-order
evaluation; the latter is called normal-order evaluation. Like most imperative languages,
Scheme uses applicative order in most cases. Normal order, which arises in the macros
and call-by-name parameters of imperative languages, is available in special cases.
Applicative and normal-order evaluation
Suppose, for example, that we have defined the following function:
(define double (lambda (x) (+ x x)))
Evaluating the expression (double (* 3 4)) in applicative order (as Scheme does), we have
(double (* 3 4))
(double 12)
(+ 12 12)
24
Under normal-order evaluation we would have
(double (* 3 4))
(+ (* 3 4) (* 3 4))
(+ 12 (* 3 4))
(+ 12 12)
24
10
Here we end up doing extra work: normal order causes us to evaluate (* 3 4) twice.
Normal-order avoidance of unnecessary work
In other cases, applicative-order evaluation can end up doing extra work. Suppose we
have defined the following:
(define switch (lambda (x a b c)
(cond ((< x 0) a)
((= x 0) b)
((> x 0) c))))
Evaluating the expression (switch -1 (+ 1 2) (+ 2 3) (+ 3 4)) in applicative order, we have
(switch -1 (+ 1 2) (+ 2 3) (+ 3 4))
(switch -1 3 (+ 2 3) (+ 3 4))
(switch -1 3 5 (+ 3 4))
(switch -1 3 5 7)
(cond ((< -1 0) 3)
((= -1 0) 5)
((> -1 0) 7))
(cond (#t 3)
((= -1 0) 5)
((> -1 0) 7))
3
(Here we have assumed that cond is built in, and evaluates its arguments lazily, even
though switch is doing so eagerly.) Under normal-order evaluation we would have
(switch -1 (+ 1 2) (+ 2 3) (+ 3 4))
(cond ((< -1 0) (+ 1 2))
((= -1 0) (+ 2 3))
((> -1 0) (+ 3 4)))
(cond (#t (+ 1 2))
((= -1 0) (+ 2 3))
((> -1 0) (+ 3 4)))
(+ 1 2)
3
Here normal-order evaluation avoids evaluating (+ 2 3) or (+ 3 4). (In this case, we have
assumed that arithmetic and logical functions such as + and < are built in, and force the
evaluation of their arguments.)
11
In our overview of Scheme we have differentiated on several occasions between special
forms and functions. Arguments to functions are always passed by sharing and are
evaluated before they are passed (i.e., in applicative order). Arguments to special forms
are passed unevaluated¡ªin other words, by name. Each special form is free to choose
internally when (and if) to evaluate its parameters. Cond, for example, takes a sequence
of unevaluated pairs as arguments. It evaluates their cars internally, one at a time,
stopping when it finds one that evaluates to #t.
Strictness and Lazy Evaluation
Evaluation order can have an effect not only on execution speed, but on program
correctness as well. A program that encounters a dynamic semantic error or an infinite
regression in an "unneeded" subexpression under applicative-order evaluation may
terminate successfully under normal-order evaluation. A (side-effect-free) function is said
to be strict if it is undefined (fails to terminate, or encounters an error) when any of its
arguments is undefined. Such a function can safely evaluate all its arguments, so its result
will not depend on evaluation order. A function is said to be nonstrict if it does not
impose this requirement¡ªthat is, if it is sometimes defined even when one of its
arguments is not. A language is said to be strict if it is defined in such a way that
functions are always strict. A language is said to be nonstrict if it permits the definition of
nonstrict functions. If a language always evaluates expressions in applicative order, then
every function is guaranteed to be strict, because whenever an argument is undefined, its
evaluation will fail and so will the function to which it is being passed. Contra-positively,
a nonstrict language cannot use applicative order; it must use normal order to avoid
evaluating unneeded arguments. ML and (with the exception of macros) Scheme are
strict. Miranda and Haskell are nonstrict.
Lazy evaluation gives us the advantage of normal-order evaluation (not evaluating
unneeded subexpressions) while running within a constant factor of the speed of
applicative-order evaluation for expressions in which everything is needed. The trick is to
tag every argument internally with a "memo" that indicates its value, if known. Any
attempt to evaluate the argument sets the value in the memo as a side effect, or returns the
value (without recalculating it) if it is already set.
Avoiding work with lazy evaluation
Returning to the expression of (double (* 3 4)) will be compiled as (double (f)), where f
is a hidden closure with an internal side effect:
(define f
(lambda ()
(let ((done #f)
; memo initially unset
(memo '())
(code (lambda () (* 3 4))))
(if done memo
; if memo is set, return it
(begin
12
(set! memo (code))
memo)))))
; remember value
; and return it
...
(double (f))
(+ (f) (f))
(+ 12 (f))
; first call computes value
(+ 12 12)
; second call returns remembered value
24
Here (* 3 4) will be evaluated only once. While the cost of manipulating memos will
clearly be higher than that of the extra multiplication in this case, if we were to replace (*
3 4) with a very expensive operation, the savings could be substantial.
8.4 Higher-Order Functions
Map function in Scheme
A function is said to be a higher-order function (also called a functional form) if it takes a
function as an argument, or returns a function as a result. The Scheme version of map is
slightly more general. Like for-each, it takes as argument a function and a sequence of
lists. There must be as many lists as the function takes arguments, and the lists must all
be of the same length. Map calls its function argument on corresponding sets of elements
from the lists:
(map * '(2 4 6) '(3 5 7)) ⇒ (6 20 42)
Where for-each is executed for its side effects, and has an implementation-dependent
return value, map is purely functional: it returns a list composed of the values returned by
its function argument.
Folding (reduction) in Scheme
Programmers in Scheme (or in ML, Haskell, or other functional languages) can easily
define other higher-order functions. Suppose, for example that we want to be able to
"fold" the elements of a list together, using an associative binary operator:
(define fold (lambda (f i l)
(if (null? l) i
; i is commonly the identity element for f
(f (car l) (fold f i (cdr l))))))
Now (fold + 0'(1 2 3 4 5)) gives us the sum of the first five natural numbers, and (fold *
1'(1 2 3 4 5)) gives us their product.
Combining higher-order functions
One of the most common uses of higher-order functions is to build new functions from
existing ones:
(define total (lambda (l) (fold + 0 l)))
13
⇒ 15
(total '(1 2 3 4 5))
(define total-all (lambda (l)
(map total l)))
(total-all '((1 2 3 4 5)
(2 4 6 8 10)
⇒ (15 30 45)
(3 6 9 12 15)))
(define make-double (lambda (f) (lambda (x) (f x x))))
(define twice (make-double +))
(define square (make-double *))
Currying
Partial application with currying
A common operation, named for logician Haskell Curry, is to replace a multiargument
function with a function that takes a single argument and returns a function that expects
the remaining arguments:
(define curried-plus (lambda (a) (lambda (b) (+ a b))))
⇒ 7
((curried-plus 3) 4)
(define plus-3 (curried-plus 3))
(plus-3 4)
⇒ 7
Among other things, currying gives us the ability to pass a "partially applied" function to
a higher-order function:
(map (curried-plus 3) '(1 2 3)) ⇒ (4 5 6)
General-purpose curry function
It turns out that we can write a general-purpose function that "curries" its (binary)
function argument:
(define curry (lambda (f) (lambda (a) (lambda (b) (f a b)))))
(((curry +) 3) 4)
⇒ 7
(define curried-plus (curry +))
Tuples as ML function arguments
ML and its descendants (Miranda, Haskell, Caml, F#) make it especially easy to define
curried functions. Consider the following function in ML:
fun plus (a, b) : int = a + b;
==> val plus = fn : int * int -> in
14
Optional parentheses on singleton arguments
We can declare a single-argument function without parenthesizing its formal argument:
fun twice n : int = n + n;
==> val twice = fn : int -> int
twice 2;
==> val it = 4 : int
We can add parentheses in either the declaration or the call if we want, but because there
is no comma inside, no tuple is implied:
fun double (n) : int = n + n;
twice (2);
==> val it = 4 : int
twice 2;
==> val it = 4 : int
double (2);
==> val it = 4 : int
double 2;
==> val it = 4 : int
Ordinary parentheses can be placed around any expression in ML.
Simple curried function in ML
Now consider the definition of a curried function:
fun curried_plus a = fn b : int => a + b;
==> val curried_plus = fn : int -> int -> int
Note the type of curried_plus: int -> int -> int groups implicitly as int -> (int -> int).
Where plus is a function mapping a pair (tuple) of integers to an integer, curried_plus is a
function mapping an integer to a function that maps an integer to an integer:
curried_plus 3;
==> val it = fn : int -> int
plus 3;
==> Error: operator domain (int * int) and operand (int) don't agree
Shorthand notation for currying
To make it easier to declare functions like curried_plus, ML allows a sequence of
operands in the formal parameter position of a function declaration:
fun curried_plus a b : int = a + b;
15
==> val curried_plus = fn : int -> int -> int
This form is simply shorthand for the declaration in the previous example; it does not
declare a function of two arguments. Curried_plus has a single formal parameter, a. Its
return value is a function with formal parameter b that in turn returns a + b.
Folding (reduction) in ML
Using tuple notation, our fold function might be declared as follows in ML:
fun fold (f, i, l) =
case l of
nil => i
| h :: t => f (h, fold (f, i, t));
==> val fold = fn : ('a * 'b -> 'b) * 'b * 'a list -> 'b
Curried fold in ML
The curried version would be declared as follows:
fun curried_fold f i l =
case l of
nil => i
| h :: t => f (h, curried_fold f i t);
==> val fold = fn : ('a * 'b -> 'b) -> 'b -> 'a list -> 'b
curried_fold plus;
==> val it = fn : int -> int list -> int
curried_fold plus 0;
==> val it = fn : int list -> int
curried_fold plus 0 [1, 2, 3, 4, 5];
==> val it = 15 : int
Note again the difference in the inferred types of the functions.
Currying in ML vs Scheme
It is of course possible to define curried_fold by nesting occurrences of the explicit fn
notation within the function's body. The shorthand notation, however, is substantially
more intuitive and convenient. Note also that ML's syntax for function
calls¡ªjuxtaposition of function and argument¡ªmakes the use of a curried function more
intuitive and convenient than it is in Scheme:
curried_fold plus 0 [1, 2, 3, 4, 5];
(* ML *)
(((curried_fold +) 0) '(1 2 3 4 5))
; Scheme
16
8.5 Theoretical Foundations
Declarative (nonconstructive) function definition
Mathematically, a function is a single-valued mapping: it associates every element in one
set (the domain) with (at most) one element in another set (the range). In conventional
notation, we indicate the domain and range of, say, the square root function by writing
We can also define functions using conventional set notation:
Unfortunately, this notation is nonconstructive: it doesn't tell us how to compute square
roots. Church designed the lambda calculus to address this limitation.
8.6 Logic Programming Concepts
Logic programming systems allow the programmer to state a collection of axioms from
which theorems can be proven. The user of a logic program states a theorem, or goal, and
the language implementation attempts to find a collection of axioms and inference steps
(including choices of values for variables) that together imply the goal. Of the several
existing logic languages, Prolog is by far the most widely used.
Horn clauses
In almost all logic languages, axioms are written in a standard form known as a Horn
clause. A Horn clause consists of a head,[2] or consequent term H, and a body consisting
of terms Bi:

H ¡û B1, B2, ¡ , Bn
The semantics of this statement are that when the Bi are all true, we can deduce that H is
true as well. When reading aloud, we say "H, if B1, B2, ¡ , and Bn." Horn clauses can be
used to capture most, but not all, logical statements.
Resolution
In order to derive new statements, a logic programming system combines existing
statements, canceling like terms, through a process known as resolution. If we know that
A and B imply C, for example, and that C implies D, we can deduce that A and B imply
D:
17
In general, terms like A, B, C, and D may consist not only of constants ("Rochester is
rainy"), but also of predicates applied to atoms or to variables: rainy(Rochester),
rainy(Seattle), rainy(X).
Unification
During resolution, free variables may acquire values through unification with expressions
in matching terms, much as variables acquire types in ML (Section 7.2.4):
8.7 Prolog
Much as a Scheme interpreter evaluates functions in the context of a referencing
environment in which other functions and constants have been defined, a Prolog
interpreter runs in the context of a database of clauses (Horn clauses) that are assumed to
be true.[3] Each clause is composed of terms, which may be constants, variables, or
structures. A constant is either an atom or a number. A structure can be thought of as
either a logical predicate or a data structure.
Atoms, variables, scope, and type
Atoms in Prolog are similar to symbols in Lisp. Lexically, an atom looks like an
identifier beginning with a lowercase letter, a sequence of "punctuation" characters, or a
quoted character string:
foo
my_Const
+
'Hi, Mom'
Numbers resemble the integers and floating-point constants of other programming
languages. A variable looks like an identifier beginning with an uppercase letter:
Foo
My_var
X
Variables can be instantiated to (i.e., can take on) arbitrary values at run time as a result
of unification. The scope of every variable is limited to the clause in which it appears.
There are no declarations. As in Lisp, type checking occurs only when a program
attempts to use a value in a particular way at run time.
Structures and predicates
Structures consist of an atom called the functor and a list of arguments:
rainy(rochester).
teaches(scott, cs254)
bin_tree(foo, bin_tree(bar, glarch))
18
Prolog requires the opening parenthesis to come immediately after the functor, with no
intervening space. Arguments can be arbitrary terms: constants, variables, or (nested)
structures. Internally, a Prolog implementation can represent a structure using Lisp-like
cons cells. Conceptually, the programmer may prefer to think of certain structures (e.g.,
rainy) as logical predicates. We use the term "predicate" to refer to the combination of a
functor and an "arity" (number of arguments). The predicate rainy has arity 1. The
predicate teaches has arity 2.
Facts and rules
The clauses in a Prolog database can be classified as facts or rules, each of which ends
with a period. A fact is a Horn clause without a right-hand side. It looks likea single term
(the implication symbol is implicit):
rainy(rochester).
A rule has a right-hand side:
snowy(X) :- rainy(X), cold(X).
The token :- is the implication symbol; the comma indicates "and." Variables that appear
in the head of a Horn clause are universally quantified: for all X, X is snowy if X is rainy
and X is cold.
It is also possible to write a clause with an empty left-hand side. Such a clause is called a
query, or a goal. Queries do not appear in Prolog programs. Rather, one builds a database
of facts and rules and then initiates execution by giving the Prolog interpreter (or the
compiled Prolog program) a query to be answered (i.e., a goal to be proven).
Queries
In most implementations of Prolog, queries are entered with a special ?- version of the
implication symbol. If we were to type the following:
rainy(seattle).
rainy(rochester).
?- rainy(C).
the Prolog interpreter would respond with
C = seattle
Of course, C = rochester would also be a valid answer, but Prolog will find seattle first,
because it comes first in the database. (Dependence on ordering is one of the ways in
which Prolog departs from pure logic; we discuss this issue further below.) If we want to
find all possible solutions, we can ask the interpreter to continue by typing a semicolon:
C = seattle ;
C = rochester
19
If we type another semicolon, the interpreter will indicate that no further solutions are
possible:
C = seattle ;
C = rochester ;
No
Similarly, given
rainy(seattle).
rainy(rochester).
cold(rochester).
snowy(X) :- rainy(X), cold(X).
the query
?- snowy(C).
will yield only one solution.
Resolution and Unification
Resolution in Prolog
The resolution principle, due to Robinson [Rob65], says that if C1 and C2 are Horn
clauses and the head of C1 matches one of the terms in the body of C2, then we can
replace the term in C2 with the body of C1. Consider the following example:
takes(jane_doe, his201).
takes(jane_doe, cs254).
takes(ajit_chandra, art302).
takes(ajit_chandra, cs254).
classmates(X, Y) :- takes(X, Z), takes(Y, Z).
If we let X be jane_doe and Z be cs254, we can replace the first term on the right-hand
side of the last clause with the (empty) body of the second clause, yielding the new rule
classmates(jane_doe, Y) :- takes(Y, cs254).
In other words, Y is a classmate of jane_doe if Y takes cs254.
Note that the last rule has a variable (Z) on the right-hand side that does not appear in the
head. Such variables are existentially quantified: for all X and Y, X and Y are classmates
if there exists a class Z that they both take.
The pattern-matching process used to associate X with jane_doe and Z with cs254 is
known as unification. Variables that are given values as a result of unification are said to
be instantiated.
20
Unification in Prolog and ML
Unification of structures in Prolog is very much akin to ML's unification of the types of
formal and actual parameters. A formal parameter of type int * 'b list, for example, will
unify with an actual parameter of type 'a * real list in ML by instantiating 'a to int and 'b
to real.
Equality and unification
Equality in Prolog is defined in terms of "unifiability." The goal =(A, B) succeeds if and
only if A and B can be unified. For the sake of convenience, the goal may be written as A
= B; the infix notation is simply syntactic sugar. In keeping with the rules above, we have
?- a = a.
Yes
% constant unifies with itself
?- a = b.
No
% but not with another constant
?- foo(a, b) = foo(a, b).
Yes
% structures are recursively identical
?- X = a.
X=a;
% variable unifies with constant
No
% only once
?- foo(a, b) = foo(X, b).
X=a;
% arguments must unify
No
% only one possibility
Unification without instantiation
It is possible for two variables to be unified without instantiating them. If we type
?- A = B.
the interpreter will simply respond
A=B
If, however, we type
?- A = B, A = a, B = Y.
(unifying A and B before binding a to A) the interpreter will respond
A=x
B=x
Y=x
In a similar vein, suppose we are given the following rules:
takes_lab(S) :- takes(S, C), has_lab(C).
21
has_lab(D) :- meets_in(D, R), is_lab(R).
(S takes a lab class if S takes C and C is a lab class. Moreover D is a lab class if D meets
in room R and R is a lab.) An attempt to resolve these rules will unify the head of the
second with the second term in the body of the first, causing C and D to be unified, even
though neither is instantiated.
Lists
List notation in Prolog
Like equality checking, list manipulation is a sufficiently common operation in Prolog to
warrant its own notation. The construct [a, b, c] is syntactic sugar for the structure .(a, .(b,
.(c, []))), where [] is the empty list and . is a built-in cons-like predicate. This notation
should be familiar to users of ML. Prolog adds an extra convenience, however: an
optional vertical bar that delimits the "tail" of the list. Using this notation, [a, b, c] could
be expressed as [a | [b, c]], [a, b | [c]], or [a, b, c | []]. The vertical-bar notation is
particularly handy when the tail of the list is a variable:
member(X, [X | _]).
member(X, [_ | T]) :- member(X, T).
sorted([]).
% empty list is sorted
sorted([_]).
% singleton is sorted
sorted([A, B | T]) :- A =< B, sorted([B | T]).
% compound list is sorted if first two elements are in order and
% remainder of list (after first element) is sorted
Here =< is a built-in predicate that operates on numbers. The underscore is a placeholder
for a variable that is not needed anywhere else in the clause. Note that [a, b | c] is the
improper list .(a, .(b, c)). The sequence of tokens [a | b, c] is syntactically invalid.
Functions, predicates, and two-way rules
One of the interesting things about Prolog resolution is that it does not in general
distinguish between "input" and "output" arguments (there are certain exceptions, such as
the is predicate described in the following subsection). Thus, given
append([], A, A).
append([H | T], A, [H | L]) :- append(T, A, L).
we can type
?- append([a, b, c], [d, e], L).
L = [a, b, c, d, e]
?- append(X, [d, e], [a, b, c, d, e]).
X = [a, b, c]
22
?- append([a, b, c], Y, [a, b, c, d, e]).
Y = [d, e]
This example highlights the difference between functions and Prolog predicates. The
former have a clear notion of inputs (arguments) and outputs (results); the latter do not. In
an imperative or functional language we apply functions to arguments to generate results.
In a logic language we search for values for which a predicate is true. (Not all logic
languages are equally flexible. Mercury, for example, requires the programmer to specify
in or out modes on arguments. These allow the compiler to generate substantially faster
code.)
Arithmetic
Arithmetic and the is predicate
The usual arithmetic operators are available in Prolog, but they play the role of
predicates, not of functions. Thus +(2, 3), which may also be written 2 + 3, is a twoargument structure, not a function call. In particular, it will not unify with 5:
?- (2 + 3) = 5.
No
To handle arithmetic, Prolog provides a built-in predicate, is, that unifies its first
argument with the arithmetic value of its second argument:
?- is(X, 1+2).
X=3
?- X is 1+2.
X=3
% infix is also ok
?- 1+2 is 4-1.
No
% first argument (1+2) is already instantiated
?- X is Y.
ERROR
% second argument (Y) must already be instantiated
?- Y is 1+2, X is Y.
Y=3
X=3
% Y is instantiated by the time it is needed
Search/Execution Order
So how does Prolog go about answering a query (satisfying a goal)? What it needs is a
sequence of resolution steps that will build the goal out of clauses in the database, or a
proof that no such sequence exists. In the realm of formal logic, one can imagine two
principal search strategies:
23


Start with existing clauses and work forward, attempting to derive the goal. This
strategy is known as forward chaining.
Start with the goal and work backward, attempting to "unresolve" it into a set of
preexisting clauses. This strategy is known as backward chaining.
If the number of existing rules is very large, but the number of facts is small, it is possible
for forward chaining to discover a solution more quickly than backward chaining. In most
circumstances, however, backward chaining turns out to be more efficient. Prolog is
defined to use backward chaining.
Search tree exploration
Because resolution is associative and commutative (Exercise 11.5), a backward-chaining
theorem prover can limit its search to sequences of resolutions in which terms on the
right-hand side of a clause are unified with the heads of other clauses one by one in some
particular order (e.g., left to right). The resulting search can be described in terms of a
tree of subgoals, as shown in Figure 8.1. The Prolog interpreter (or program) explores
this tree depth first, from left to right. It starts at the beginning of the database, searching
for a rule R whose head can be unified with the top-level goal. It then considers the terms
in the body of R as subgoals, and attempts to satisfy them, recursively, left to right. If at
any point a subgoal fails (cannot be satisfied), the interpreter returns to the previous
subgoal and attempts to satisfy it in a different way (i.e., to unify it with the head of a
different clause).
Figure 8.1: Backtracking search in Prolog. The tree of potential resolutions consists of
alternating AND and OR levels. An AND level consists of subgoals from the right-hand
side of a rule, all of which must be satisfied. An OR level consists of alternative database
clauses whose head will unify with the subgoal above; one of these must be satisfied. The
24
notation _C = _X is meant to indicate that while both C and X are uninstantiated, they
have been associated with one another in such a way that if either receives a value in the
future it will be shared by both.
8.8 Theoretical Foundations
Predicates as mathematical objects
In mathematical logic, a predicate is a function that maps constants (atoms) or variables
to the values true and false. If rainy is a predicate, for example, we might have
rainy(Seattle) = true and rainy(Tijuana) = false. Predicate calculus provides a notation
and inference rules for constructing and reasoning about propositions (statements)
composed of predicate applications, operators (and, or, not, etc.), and the quantifiers ∀
and ∃ . Logic programming formalizes the search for variable values that will make a
given proposition true.
8.9 Logic Programming in Perspective
In the abstract, logic programming is a very compelling idea: it suggests a model of
computing in which we simply list the logical properties of an unknown value, and then
the computer figures out how to find it (or tells us it doesn't exist). Unfortunately, the
current state of the art falls quite a bit short of the vision, for both theoretical and
practical reasons.
Execution Order
While logic is inherently declarative, most logic languages explore the tree of possible
resolutions in deterministic order. Prolog provides a variety of predicates, including the
cut, fail, and repeat, to control that execution order (Section 11.2.6). It also provides
predicates, including assert, retract, and call, to manipulate its database explicitly during
execution.
Sorting incredibly slowly
one must often consider execution order to ensure that a Prolog search will terminate.
Even for searches that terminate, naive code can be very inefficient. Consider the
problem of sorting. A natural declarative way to say that L2 is the sorted version of L1 is
to say that L2 is a permutation of L1 and L2 is sorted:
declarative_sort(L1, L2) :- permutation(L1, L2), sorted(L2).
permutation([], []).
permutation(L, [H | T]) :- append(P, [H | S], L), append(P, S, W),
permutation(W, T).
Unfortunately, Prolog's default search strategy will take exponential time to sort a list
based on these rules: it will generate permutations until it finds one that is sorted.
Quicksort in Prolog
25
To obtain a more efficient sort, the Prolog programmer must adopt a less natural,
"imperative" definition:
quicksort([], []).
quicksort([A | L1], L2) :- partition(A, L1, P1, S1),
quicksort(P1, P2), quicksort(S1, S2), append(P2, [A | S2], L2).
partition(A, [], [], []).
partition(A, [H | T], [H | P], S) :- A >= H, partition(A, T, P, S).
partition(A, [H | T], P, [H | S]) :- A =< H, partition(A, T, P, S).
Even this sort is less efficient than one might hope in certain cases. When given an
already-sorted list, for example, it takes quadratic time, instead of O(n log n). A good
heuristic for quicksort is to partition the list using the median of the first, middle, and last
elements. Unfortunately, Prolog provides no easy way to access the middle and final
elements of a list (it has no arrays).
it can be useful to distinguish between the specification of a program and its
implementation. The specification says what the program is to do; the implementation
says how it is to do it. Horn clauses provide an excellent notation for specifications.
When augmented with search rules (as in Prolog) they allow implementations to be
expressed in the same notation.
Negation and the "Closed World" Assumption
Negation as failure
A collection of Horn clauses, such as the facts and rules of a Prolog database, constitutes
a list of things assumed to be true. It does not include any things assumed to be false.
This reliance on purely "positive" logic implies that Prolog's \+ predicate is different from
logical negation. Unless the database is assumed to contain everything that is true (this is
the closed world assumption), the goal \+(T) can succeed simply because our current
knowledge is insufficient to prove T. Moreover, negation in Prolog occurs outside any
implicit existential quantifiers on the right-hand side of a rule. Thus
?- \+(takes(X, his201)).
where X is uninstantiated, means
? ¬∃ X[takes(X, his201)]
rather than
? ∃ X[¬takes(X, his201)]
If our database indicates that jane_doe takes his201, then the goal takes(X, his201) can
succeed, and \+(takes(X, his201)) will fail:
?- \+(takes(X, his201)).
No
26
If we had a way to put the negation inside the quantifier, we might hope for an
implementation that would respond
?- \+(takes(X, his201)).
X = ajit_chandra
or even
?- \+(takes(X, his201)).
X != jane_doe
A complete characterization of the values of X for which ¬takes(X, his201) is true would
require a complete exploration of the resolution tree, something that Prolog does only
when all goals fail, or when repeatedly prompted with semicolons. Mechanisms to
incorporate some sort of "constructive negation" into logic programming are an active
topic of research.
Negation and instantiation
It is worth noting that the definition of \+ in terms of failure means that variable bindings
are lost whenever \+ succeeds. For example,
?- takes(X, his201).
X = jane_doe
?- \+(takes(X, his201)).
No
?- \+(\+(takes(X, his201))).
Yes
% no value for X provided
When takes first succeeds, X is bound to jane_doe. When the inner \+ fails, the binding is
broken. Then when the outer \+ succeeds, a new binding is created to an uninstantiated
value. Prolog provides no way to pull the binding of X out through the double negation
27