Survey
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
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
Lambda lifting wikipedia , lookup
Lambda calculus definition wikipedia , lookup
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