CSI 311 Principles of Programming Languages


  • Notes on Example Syntax Differences

    Here are four programs from four different languages. All four programs compute the Nth Fibonacci number. The first two do so in essentially identical ways: using loops in QBASIC and in LISP. The third program is Prolog and is recursive because ordinary iteration is impossible in Prolog. But the recursive style is about as close to iteration as can be done in Prolog. The complexity is linear. The fourth program is C++. It is recursive, and it is also inefficient, having exponential complexity. Note the drastic differences in the syntax of the four languages. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Compute the N-th Fibonacci Number (QBASIC) SUB fibonacci (N) IF N=0 THEN PRINT 0 IF N=1 THEN PRINT 1 IF N>1 THEN N1 = 0 N2 = 1 S = 1 I = 1 DO S = N1 + N2 N1 = N2 N2 = S I = I + 1 UNTIL I = N PRINT S END IF END SUB Compute the N-th Fibonacci Number (LISP) (defun fibonacci (n) (cond ((eq n 0) 0) ((eq n 1) 1) (T (prog ((nextlast 0) (last 1) (index 1) ) A (setf index (+ index 1)) (setf fib (+ last nextlast)) (setf last fib) (setf nextlast last) (cond ((< index N) (go A))) )) ) fib ) Compute the N-th Fibonacci Number (Prolog) fib(0, 0) :- !. fib(1, 1) :- !. fib(N, FIB) :- fastfib(0, 1, 1, 1, N, FIB). fastfib(N2, F2, N1, F1, N1, F1) :- !. fastfib(N2, F2, N1, F1, N, FIB) :- NewF is F2+F1, NewN is N1+1, fastfib(N1, F1, NewN, NewF, N, FIB). Compute the N-th Fibonacci Number (C++) int fibonacci (int n) { if (n == 0 || n == 1) return 1; else return fibonacci(n-2) + fibonacci(n-1); }

  • Notes on BNF Grammars

    Notes on Formal Languages, Grammars, and Backus-Naur Form A VOCABULARY is a finite set of symbols. A SENTENCE is a finite sequence of symbols taken from a vocabulary. A LANGUAGE is a (possibly infinite) set of sentences. A GRAMMAR G is a 4-tuple G = <P, V, v, S> where P = a finite set of rules (called production rules) V = a vocabulary of non-terminal symbols v = a vocabulary of terminal symbols S = a distinguished symbol called the start symbol A production rule is of the form: L ::= R, where L and R are finite sequences of symbols and the symbol "::=" means "can be rewritten as." A production rule L ::= R is CONTEXT-FREE if L is a single non-terminal. A grammar G is context-free if all it's production rules are context-free. A DERIVATION using grammar G is a finite sequence of sentences, D(G) = S0, S1, S2, ... , Sn, such that: 1. S0 is the start symbol of G 2. Each Si is obtained from S(i-1) by application of a rule from G. I.e., S(i-1) is a sentence of the form aLb, Si is a sentence of the form aRb, and G contains the rule L ::= R. We say that D(G) is a derivation of Sn. D(G) is a CANONICAL derivation if each application of a rule is performed on the leftmost non-terminal in S(i-1) to produce Si. Notation: To indicate that sentence Sj can be derived from sentence Si, * we write Si ===> Sj G If Sn consists of only terminal symbols, then Sn is in the language generated by G. The language generated by G is denoted by L(G); * L(G) = { w | S ===> w, w consists of terminal symbols only } G The Language of Balanced Parentheses Examples: (()()) ()((())) ((()())(()))() A grammar that defines the language whose sentences are strings of balanced parentheses. Terminals: { (, ) } Non-terminals: {S, X} (S = start symbol) Production Rules: S ::= X X ::= () X ::= (X) X ::= XX Derive ( ( ( ) ( ) ) ( ( ) ) ) ( ) | | |__| |__| | | |__| | | |__| | |______________| |________| | |________________________________| S X X X X ( ) ( X ) ( ) ( X X ) ( ) ( X ( X ) ) ( ) ( X ( ( ) ) ) ( ) ( ( X ) ( ( ) ) ) ( ) ( ( X X ) ( ( ) ) ) ( ) ( ( ( ) X ) ( ( ) ) ) ( ) ( ( ( ) ( ) ) ( ( ) ) ) ( ) Example: Integers without leading zeros Examples: 712, +44, -8787, 900801 Terminals: {+, -, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9} Non-terminals: {S, X, D, Y, Z} Grammar: S ::= 0 S ::= X S ::= +X S ::= -X Y ::= Z Y ::= ZY X ::= D X ::= DY D ::= 1 . . . D ::= 9 Z ::= 0 Z ::= D S .......... Start Sym. \- all integer constants X .......... Any integer without leading zeros Y .......... all strings of digits D .......... all non-zero digits Z .......... all digits Derivation S +X +DY +4Y +4Z +4D +44 Parse Trees --------------------- A PARSE TREE for a sentence Sn in L(G), where G is a context-free grammar, is a tree in which: 1. Each node in the tree is a symbol from G 2. The root is the start symbol for G 3. If node L has children R1, R2, ... , Rn, then G contains the rule L ::= R where R = R1 R2 ... Rn 4. If node L is a leaf, then L is a terminal symbol. 5. If the leaves of the tree are L1, L2, ... , Ln in left-to-right order, then Sn = L1 L2 ... Ln A context-free grammar G is UNAMBIGUOUS if for every sentence w in L(G), there is a unique parse tree for w (hence a unique canonical derivation). A given language will in general be generated by many grammars; some may be ambiguous and some may not. An INHERENTLY AMBIGUOUS language is one for which there does not exist an unambiguous grammar. Canonical Derivation Parse Tree S S +X / \ +DY + X +4Y / \ +4Z D Y +4D | | +44 4 Z | D | 4 Backus-Naur Form (BNF) ----------------------- This is just some convenient notation for describing a context-free grammar more succinctly. Non-terminal symbols are recognized by being enclosed in angle-brackets. Terminal symbols are recognized by NOT being enclosed in angle-brackets. The "|" symbol is used on the right hand side of rules to indicate alternative choices. There is no special declaration of the start symbol; it is usually obvious. Example: G = <P, V, v, S> where P = {X ::= A, V = {X} S = X X ::= B, X ::= C} v = {A, B, C} In BNF, we can specify G by the one rule: <X> ::= A | B | C Integers without leading zeros using BNF <S> ::= <X> | +<X> | -<X> | 0 <X> ::= <D> | <D><Y> <Y> ::= <Z> | <Z><Y> <D> ::= 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 <Z> ::= 0 | <D> Context-Sensitive Grammars The language L = { ww | w is in {a,b}+ } is not context-free: No context-free grammar generates L. If we allow rules in which the left hand side consists of several symbols, and the right hand sides are at least as big as the left, we get CONTEXT-SENSITIVE grammars that generate context-sensitive languages. The language L is a context-sensitive language. Consider the grammar G: <S> ::= <C> <M> <E> <C><M> ::= a<M><A> | b<M><B> | a<C><M><A> | b<C><M><B> <A><E> ::= a<E> <B><E> ::= b<E> <A>a ::= a<A> <B>a ::= a<B> <A>b ::= b<A> <B>b ::= b<B> <E> ::= empty <M> ::= empty G is almost context-sensitive; the last two rules violate the restriction on the size of the right hand sides of rules. But G can be easily converted to an equivalent grammar that is strictly context-sensitive. (Left as an exercise.)


  • Notes on Value by Reference, Value, and Name The following three programs in BASIC, FORTRAN, and AlgolW look quite similar. But they all produce different output. Why? The answer lies in subtle differences in the semantics of the three languages. In particular, the conventions involving the scopes of variables and the parameter passing mechanisms are different in the various languages. If we simulate all three programs in Pascal, then the differences become apparent. In Pascal, variables must be declared local; they are taken from the "nearest" global context in which they were declared local otherwise. Parameters must be declared as "var" parameters if the pass by reference mechanism is to be used. Otherwise, they default to pass by value. Pass by name is not directly expressible in Pascal, but the effect is simulated with code specific to the AlgolW example.

    BASIC, FORTRAN, & ALGOLW Simulations

    BASIC

    I = 1 : J = 2 : K = 3

    D := FNW(I, J-2)

    PRINT I

    DEF FNW(L, M)

    L = 10 : J = 6

    PRINT M : FNW = 0

    FNEND

    END output -> 1

    -> 0

    program basic(input, output);

    var I, J, K : integer; D : real;

    function W(L, M : integer) : real;

    begin

    L := 10; J := 6;

    writeln(M); { executable Pascal }

    W := 0

    end;

    begin

    I := 1; J := 2; K := 3;

    D := W(I, J-2);

    writeln(I)

    end.


    FORTRAN
    I = 1 FUNCTION W(L, M)

    J = 2 L = 10

    K = 3 J = 6

    D = W(I, J-2) WRITE(4, 15)

    15 WRITE(4, 15) I W = 0

    FORMAT(1X, I7) END

    END output -> 10

    0

    program fortran(input, output);

    var I, J, K : integer; D : real;

    temp : integer;

    function W(var L, M : integer) : real;

    var J : integer;

    begin

    L := 10; J := 6;

    writeln(M); { executable Pascal }

    W := 0

    end;

    begin

    I := 1; J := 2; K := 3;

    temp := J-2; D := W(I, temp);

    writeln(I)

    end.


    ALGOL W
    INTEGER I,J,K; REAL D;

    I := 1; J := 2; K := 3;

    D := W(I, J-2);

    WRITE I;

    REAL PROCEDURE W(INTEGER L, M);

    BEGIN

    L := 10; J := 6;

    WRITE M;

    0

    END; output -> 10

    4

    program algolw(input, output);

    var I, J, K : integer; D : real;

    function W(L, M : integer) : real;

    begin

    {L is replaced by I} I := 10; J := 6;

    writeln({M is replaced by J-2} J-2);

    W := 0 { executable Pascal )

    end;

    begin

    I := 1; J := 2; K := 3;

    D := W(I, J-2);

    writeln(I)

    end.


  • Notes on Attribute grammars The following notes are to clarify some details involving attribute grammars.

    Consider the language L = {abc, aabbcc, aaabbbccc, ...}. L is not a context free language and to specify it, we need either a context-sensitive, or an attribute, grammar. We will develop an attribute grammar such that any parse tree for a sentence of L has a synthesized attribute that equals the number of a's (and thus also the #b's and #c's) in the sentence.

    Clearly, we need a start symbol and a rule to get us going. We use <S> for the start symbol; the rule must have a synthesized attribute on the left side that reflects the number of a's. This is like a formal parameter in the declaration of a procedure that is used as an output parameter. The right side of the rule is like the body of a procedure definition, where we do computation and invoke other procedures (here, the other procedures are syntactic categories, i.e., non-terminals that, like <S>, are defined through rules). We'll call our synthesized attribute n, and THROUGHOUT THIS RULE, wherever we mention n, it is the SAME n. We're not sure about all the details of the rule for <S> just yet, but the right side must at least generate 3 strings composed of a's, b's, and c's respectively. So what appears below is just a first cut, NOT THE FINAL RULE for <S>.

    <S> ^n ::= <A> <B> <C>

    OK, so now what? Well, let's think about the attributes. Somewhere in the sub-parse-tree below <S>, we must synthesize n. In fact, it had better be synthesized in the sub-parse-tree below <A>, because we'll need it to be sure that the strings generated from <B> and <C> are acceptable. That means that n will be an INHERITED attribute in the rules for <B> and <C>.

    <S> ^n ::= <A> ^n <B> !n <C> !n

    What about having synthesized attributes for <B> and <C> - do we need them? Not here; we have enough information available once we have synthesized n. But somehow the rules for <B> and for <C> will need to check the length of THEIR OWN generated strings to ensure that the lengths are correct. This could be done by having the rules for <B> and <C> synthesize an attribute that represents this length, and then compare this synthesized attribute with n, the inherited one representing the number of a's. If we were to do this, the synthesized attributes for <B> and <C> would have to be shown in the rule for <s>, even though we don't use them here As it turns out, I will manage to construct the rules for <B> and <C> without returning a synthesized attribute; this is done by postponing the test until the rules for <B> and <C>are generating a single b or c.

    Can we use the attribute n as both a synthesized and inherited attribute in the same rule? Yes, but the key word is USE; remember that n is like a declared formal parameter on the left, but we're merely USING it in the "procedure body" on the right. It can be thought of as an actual parameter or argument to the "procedures" for <A>, <B>, and <C> on the right. Just as parameters in a procedure call must match those in the declaration, the "procedure" or rule for <A> must specify a single SYNTHESIZED attribute on the left, while the rules for <B> and <C> must specify a single INHERITED attribute on the left. I.e., since we "call" the rule for <B> with an inherited attribute (one specified with !), the "definition" of <B> must expect an inherited attribute. We are free to use any attribute we have on hand in specifying the "call", but if the called rule expects an inherited value, then the attribute we use must be already assigned a value. If the called rule expects a synthesized attribute, then the attribute used in the call may be undefined, or may have its value changed. The situation is very much like that involving the use of var versus ordinary parameters in Pascal.

    If these rules really were Pascal-like procedures, we would have something like:

    procedure S(var n:integer); procedure A(var aval:integer); begin begin A(n); B(n); C(n); {generate a string of a's} end; aval := {number of a's} end; procedure B(bval:integer); procedure C(cval:integer); begin begin {generate string of bval b's} {generate string of cval c's} end; end;

    Notice that n is a var parameter of S and is used in the call to A where it matches the var parameter aval. It is used in the calls to B and C only after being assigned a value where it matches the VALUE parameters bval and cval respectively. This suggests how we might start writing the rules for <A>, <B>, and <C>:

    <A> ^aval ::= a ^1 | a <A> ^n ADD1 !n ^aval

    ADD1 !in ^out ::= out := in + 1;

    The synthesized attribute aval in this rule matches the corresponding attribute n in the body of the rule for <s>, where <A> is "called". The recursive call to <A> determines the attribute n in this rule which has no connection to attribute n in the rule for <s> - it's like a local variable.

    There's another little shortcut taken here that is potentially confusing. Look at the very first alternative in the rule for <A>. It says that the string "a" is generated and the attribute value 1 is synthesized. But what happens to the 1? Most people would correctly guess that it is used as the synthesized value returned for aval; aval is, after all, the only attribute synthesized by the rule itself. But to be completely formal, we should show aval being assigned the value 1:

    <A> ^aval ::= a ASSIGN ^aval !1 | a <A> ^n ADD1 !n ^aval ASSIGN ^id !exp ::= id := exp; ADD1 !in ^out ::= out := in + 1; Now we have shown explicitly how the attribute aval gets its value in the first alternative of the rule; this was already explicit in the second alternative, through the ADD1 evaluation rule. (The previous version of this rule is perfectly acceptable and is considered correct.) Now the rules for <B> and <C>: <B> !bval ::= b CONDITION !bval bval = 1 | b SUB1 !bval ^newb <B> !newb SUB1 !in ^out ::= out := in - 1;

    The rule for <B> is supplied with the inherited attribute bval and is supposed to generate only strings of exactly bval b's. A single b is allowed if the inherited bval equals 1. Or a single b followed by a string of (bval-1) b's is ok, so we generate one b, compute the new attribute newb, and hand it off to the recursive call on the <B> rule. Notice that CONDITIONs and attribute evaluation rules like ADD1 and SUB1 must have their inherited and synthesized attributes specified just like non-terminals. These computations can be regarded as non-terminals that always generate the empty string, but that also test and compute attribute values. At this point, the rule for <C> is obvious: <C> !cval ::= c CONDITION !cval cval = 1 | c SUB1 !cval ^newc <C> !newc Below we show a partially completed parse tree for the sentence "aabbcc". Please excuse the absence of artistic proficiency. <s> ^n

    . . .

    . . .

    . . .

    <A> ^2 <B> !2 <C> !2

    . . . | |

    . . . etc. etc.

    a ^1 <A> ^1 .

    . ADD1 !1 ^2

    .

    a ^1

    The Double Word Example

    Here is an attribute grammar for the language L = { W | W=ww, where w is in {a,b}* }

    <dwd>^n1::= <wd>^n2 <ckwd>!n2 ^n3 CONC!n2 !n3 ^n1

    <wd>^n1 ::= <let>^n1 |<wd>^n2 <let>^n3 CONC!n2 !n3 ^n1

    <ckwd>!n2 ^n1 ::= <wd>^n3 COND: n3=n2

    <let>^n1 ::= a^"a" | b^"b"

    CONC!n2 !n3 ^n1 {strings n2 and n3 are concatenated producing n1}


    Question: Can we show a derivation of a string using an attribute grammar, rather than showing a parse tree for that string? The answer is yes. Below is a derivation of "abab". Normally a parse tree would be preferred because attribute value relationships and computations are displayed much more clearly. A derivation contains all this information (provided that you always know precisely which non-terminal is selected at each step as in, e.g., a canonical derivation), but that information is rather well hidden. Anyway, here goes:

    <dwd>^__

    <wd>^__ <ckwd>!__ ^__CONC!__ !__ ^__

    <wd>^__<let> ^__CONC!__ !__ ^__<ckwd>!__ ^__CONC!__ !__ ^__

    <let>^__<let>^__CONC!__ !__ ^__<ckwd>!__ ^__CONC!__ !__ ^__

    a^"a" <let>^__CONC!"a"!__ ^__<ckwd>!__ ^__CONC!__ !__ ^__

    a^"a" b^"b" CONC!"a"!"b"^__<ckwd>!__ ^__CONC!__ !__ ^__

    a^"a" b^"b" CONC!"a"!"b"^"ab"<ckwd>!"ab"^__CONC!"ab"!__ ^__

    Notice that I have tried to align corresponding attribute slots vertically (think of the arguments in a call to a procedure corresponding to the formal parameters in its definition). This explains why the non-terminal <dwd>^__ is over on the right in the first line of the derivation: its synthesized attribute gets its value from the third attribute of CONC in the next step.

    Note also that this is a canonical derivation, and that the concatenation of "a" and "b" is shown as a separate step. Remember that CONC!"a"!"b"^"ab" is just an evaluation rule, so, while we show it in the derivation, it is NOT part of the actual string of terminal symbols that we derive.

    Since the lines of the derivation are becoming too long, we will continue by omitting the part on the left in which no non-terminals remain and all attributes are computed (this omitted part is ` a^"a" b^"b" CONC!"a"!"b"^"ab" ').

    <ckwd>!"ab"^__CONC!"ab"!__ ^__

    <wd>^__COND:"ab"=__CONC!"ab"!__ ^__

    <wd>^__<let> ^__CONC!__ !__ ^__ COND:"ab" = __ CONC!"ab"!__ ^__

    <let>^__<let> ^__CONC!__ !__ ^__ COND:"ab" = __ CONC!"ab"!__ ^__

    a^"a" <let> ^__CONC!"a"!__ ^__ COND:"ab" = __ CONC!"ab"!__ ^__

    a^"a" b^"b" CONC!"a"!"b"^__ COND:"ab" = __ CONC!"ab"!__ ^__ a^"a" b^"b" CONC!"a"!"b"^"ab"COND:"ab"="ab" CONC!"ab"!"ab"^__

    a^"a" b^"b"CONC!"a"!"b"^"ab" COND:"ab"="ab" CONC!"ab"!"ab"^"abab"

    Formally, the omitted part is carried along at the left in each of the above 7 steps. It's now clear that although derivations are perfectly well defined entities for attribute grammars, parse trees are by far preferable. Here's the simple example done in class on expressions of mixed type. ::= ^t ^t | ^t1 * ^t2 combinetypes: !t1 !t2 ^t | ^t1 / ^t2 combinetypes: !t1 !t2 ^t ::= I ^"int" | J ^"int" | ... | N ^"int" | A ^"real" | B ^"real" | ... | H ^"real" Evaluation rule: combinetypes under !arg1 !arg2 ^result ::= if arg1 = "real" then result <- "real" else if arg1 = "int" then result <- arg2 (We use `<-' for assignment in the evaluation rule.) Can we draw a parse tree for "A / I * J" ? (You should fill in the missing atribute values.) ^__ . . . . . . . . . . . . . . . . . . . . . ^__ / ^__ combinetypes !__ !__ ^__ | . | . . . . A^"real" . . . . . . . . . . . . . . . . ^__ * ^__ combinetypes !__ !__ ^__ | | | | I^"int" ^__ | | J^"int"


  • Notes on Parameter-Passing Mechanisms NOTES ON PARAMETER - PASSING MECHANISMS These notes help to explain the behavior of the programs exhibited in the file "notes.ValRefName" - there, three kinds of parameter passing mechanisms are illustrated: Value, Reference, and Name. Here, we give a more formal explanation of these three plus two more: Result and Value-Result. Note: We may not have time to cover this material prior to Exam 1. In that case, we'll do it after the exam, and Exam 1 will not include this material, but Exam 2 may.

    Names, l-values, r-values var I, J : integer; A : array[1..100] of integer; begin J := 3; I := J + 1; A[I] := I; . . . name l-value r-value (environment) (store) =================================================== ____________ I 05834726 | 4 | ------------ ____________ J 05834727 | 3 | ------------ ____________ A ([1]) 05834728 | | ----------- ____________ | | ------------ ____________ | | ------------- ____________ A ([4]) 05834731 | 4 | ------------ ____________ 3 00000001 | 3 | ------------

    PASS BY VALUE

    * One-way communication * Provides input to the called procedure * Protects the actual parameters om change When a procedure is called: 1. An environment and a store are allocated for the called procedure, including the formal parameters and locally declared variables. 2. The r-values of the actual parameters are copied into locations specified by the l-values of the formal parameters. When control returns from a called procedure: 1. The environment and store for the called procedure is deallocated. PASS BY VALUE program I : integer; A : array[1..100] of integer; procedure VAL_SWAP(value X, Y : integer); var temp : integer; begin temp := X; X := Y; Y := temp; end; begin {main program} I := 3; A[I] := 6; write('I =', I); A[3]); writeln('A[3] =', A[3]); VAL_SWAP(I, A[I]); { --->>> temp:=X; X:=Y; Y:=temp;} write('I =', I); A[3]); writeln('A[3] =', A[3]); end. OUTPUT I = 3 A[3] = 6 I = 3 A[3] = 6 PASS BY RESULT * One-way communication * Provides output to the calling procedure * Protects the formal parameters om initialization by the calling routine When a procedure is called: 1. An environment and a store are allocated for the called procedure, including the formal parameters and locally declared variables. 2. The l-values of the actual parameters are saved, one for each of the formal parameters. When control returns from a called procedure: 1. The r-values of the formal parameters are copied into locations specified by the saved l-values of the actual parameters. 2. The environment and store for the called procedure is deallocated. PASS BY RESULT program I : integer; A : array[1..100] of integer; procedure RES_SWAP(result X, Y : integer); var temp : integer; begin temp := X; X := Y; Y := temp; end; begin {main program} I := 3; A[I] := 6; write('I =', I); A[3]); writeln('A[3] =', A[3]); RES_SWAP(I, A[I]); { --->>> temp:=X; X:=Y; Y:=temp;} X:=Y; Y:=temp; write('I =', I); A[3]); writeln('A[3] =', A[3]); end. OUTPUT I = 3 A[3] = 6 * * * ERROR : UNDEFINED VARIABLE X PASS BY VALUE-RESULT * Two-way communication * Provides both input to, and output from the called procedure * No protection for formal or actual parameters When a procedure is called: 1. An environment and a store are allocated for the called procedure, including the formal parameters and locally declared variables. 2. The l-values of the actual parameters are saved, one for each of the formal parameters. 3. The r-values of the actual parameters are copied into locations specified by the l-values of the formal parameters. When control returns from a called procedure: 1. The r-values of the formal parameters are copied into locations specified by the saved l-values of the actual parameters. 2. The environment and store for the called procedure is deallocated. PASS BY VALUE-RESULT program I : integer; A : array[1..100] of integer; procedure V_R_SWAP(val_res X, Y : integer); var temp : integer; begin temp := X; X := Y; Y := temp; end; begin {main program} I := 3; A[I] := 6; write('I =', I); A[3]); writeln('A[3] =', A[3]); V-R_SWAP(I, A[I]); { --->>> temp:=X; X:=Y; Y:=temp;} X:=Y; Y:=temp; write('I =', I); A[3]); writeln('A[3] =', A[3]); end. OUTPUT I = 3 A[3] = 6 I = 6 A[3] = 3 PASS BY REFERENCE * Two-way communication * Provides both input to, and output from the called procedure * No protection for formal or actual parameters * Almost equivalent to value-result parameters; discrepancies show up in the presence of aliasing. When a procedure is called: 1. An environment and a store are allocated for the called procedure, including the formal parameters and locally declared variables. 2. The l-values of the formal parameters are set equal to the l-values of the actual parameters. When control returns from a called procedure: 1. The environment and store for the called procedure is deallocated. PASS BY REFERENCE program I : integer; A : array[1..100] of integer; procedure REF_SWAP(var X, Y : integer); var temp : integer; begin temp := X; X := Y; Y := temp; end; begin {main program} I := 3; A[I] := 6; write('I =', I); A[3]); writeln('A[3] =', A[3]); REF_SWAP(I, A[I]); { --->>> temp:=X; X:=Y; Y:=temp;} X:=Y; Y:=temp; write('I =', I); A[3]); writeln('A[3] =', A[3]); end. OUTPUT I = 3 A[3] = 6 I = 6 A[3] = 3 PASS BY NAME * One or two-way communication. If an actual parameter has no l-value, then assignment to the corresponding formal parameter is disallowed. * Delayed evaluation of actual parameters - by need only. * No protection for formal or actual parameters When a procedure is called: 1. An environment and a store are allocated for the called procedure, including the locally declared variables. 2. If necessary, local variables are given new names not appearing elsewhere in the program. 2. The literal text of the actual parameters replaces each occurrence of the corresponding formal parameters. Variables mentioned in these expressions are global to the called procedure. When control returns "from" a called procedure: 1. The environment and store for the called procedure is deallocated. PASS BY NAME program I : integer; A : array[1..100] of integer; procedure NAM_SWAP(name X, Y : integer); var temp : integer; begin temp := X; X := Y; Y := temp; end; begin {main program} I := 3; A[I] := 6; write('I =', I); A[3]); writeln('A[3] =', A[3]); NAM_SWAP(I, A[I]); { --->>> temp:=I; I:=A[I]; A[I]:=temp; } write('I =', I); A[3]); writeln('A[3] =', A[3]); end. OUTPUT I = 3 A[3] = 6 I = 6 A[3] = 6

    The following three programs in BASIC, FORTRAN, and AlgolW look quite similar. But they all produce different output. Why?

    The answer lies in subtle differences in the semantics of the three languages. In particular, the conventions involving the scopes of variables and the parameter passing mechanisms are different in the various languages. If we simulate all three programs in Pascal, then the differences become apparent.

    In Pascal, variables must be declared local; they are taken from the "nearest" global context in which they were declared local otherwise.

    Parameters must be declared as "var" parameters if the pass by reference mechanism is to be used. Otherwise, they default to pass by value. Pass by name is not directly expressible in Pascal, but the effect is simulated with code specific to the AlgolW example.

    BASIC, FORTRAN, & ALGOLW Simulations BASIC 10 I = 1 : J = 2 : K = 3 20 D := FNW(I, J-2) 30 PRINT I 100 DEF FNW(L, M) 110 L = 10 : J = 6 120 PRINT M : FNW = 0 140 FNEND 999 END output -> 0 1 program basic(input, output); var I, J, K : integer; D : real; function W(L, M : integer) : real; begin L := 10; J := 6; writeln(M); { executable Pascal } W := 0 end; begin I := 1; J := 2; K := 3; D := W(I, J-2); writeln(I) end. FORTRAN I = 1 FUNCTION W(L, M) J = 2 L = 10 K = 3 J = 6 D = W(I, J-2) WRITE(4, 15) M 15 WRITE(4, 15) I W = 0 FORMAT(1X, I7) END END output -> 0 10 program fortran(input, output); var I, J, K : integer; D : real; temp : integer; function W(var L, M : integer) : real; var J : integer; begin L := 10; J := 6; writeln(M); { executable Pascal } W := 0 end; begin I := 1; J := 2; K := 3; temp := J-2; D := W(I, temp); writeln(I) end. ALGOL W INTEGER I,J,K; REAL D; I := 1; J := 2; K := 3; D := W(I, J-2); WRITE I; REAL PROCEDURE W(INTEGER L, M); BEGIN L := 10; J := 6; WRITE M; 0 END; output -> 4 10 program algolw(input, output); var I, J, K : integer; D : real; function W(L, M : integer) : real; begin {L is replaced by I} I := 10; J := 6; writeln({M is replaced by J-2} J-2); W := 0 { executable Pascal } end; begin I := 1; J := 2; K := 3; D := W(I, J-2); writeln(I) end.


  • Notes on Axiomatic semantics AXIOMATIC SEMANTICS

    Axiomatic semantics is based on logic. The logical expressions are called assertions; an assertion is a true-false statement about the state of the program. An assertion will typically be an equality or inequality relating the values of various identifiers in the code. The main goal of axiomatic semantics is to construct an assertion {C} that defines the correct behavior of a segment of code S. We want C to be true AFTER executing S; so we must discover the preconditions {P} that must hold PRIOR to executing S, in order to guarantee that {C}, the desired postconditions, will hold AFTER executing S. The preconditions {P} describe those constraints that must be satisfied in order for S to behave correctly. When we are lucky, {C} will always be true after executing S; in this case, we will discover that the preconditions {P} are trivially true. This means that the code is correct under all circumstances. So we need axioms and rules that relate postconditions to preconditions and vice versa. Since we want to be able to do this for arbitrary segments of code, we need such rules, in principle, for every kind of statement in the programming language under consideration. However, if we have rules for a small number of primitive statements, and if we can define other statements in terms of those primitives, then in principle we have enough rules. For example, the assignment statement: {P}[exp/id] id := exp {P} This axiom means that for {P} to be true after execution of "id := exp", then {P}[exp/id] must be true prior to execution of "id := exp", where {P}[exp/id] means substitute exp for all occurrences of id in {P}. For instance, suppose we want {sum > 1} to be true after executing "sum := 2*x + 1". What must be true prior to executing the assignment in order to guarantee {sum > 1} after? Well, from the assignment axiom, {sum > 1}[2*x + 1 / sum] sum := 2*x + 1 {sum > 1} and {sum > 1}[2*x + 1 / sum] = {2*x + 1 > 1} = {2*x > 0} = {x > 0} so if {x > 0} is true and we execute sum := 2*x + 1, then {sum > 1} is true. NOTE: if we know beforehand that {x > 0}, and we want to know what is true after executing "sum := 2*x + 1", then we can apply the substitution in reverse if we're careful. So we look for occurrences "2*x + 1" in {x > 0} and we don't see any. But {x > 0} = {2*x > 2*0} = {2*x + 1 > 2*0 + 1} = {2*x + 1 > 1} so {2*x + 1 > 1}[sum / 2*x + 1] = {sum > 1} and we know that {sum > 1} will hold afterward. Now, we talked about code segments, not single statements. What if we have a sequence of statements? Well, informally we can apply axioms or rules for single statements and successively generate assertions further and further back. The relationship of one assertion being guaranteed true if another is also true is TRANSITIVE. More formally we have the COMPOSITION RULE: {P} S1 {Q} and {Q} S2 {R} ----------------------------- {P} S1 ; S2 {R} This notation means that if we can prove everything in the upper part of the rule, then we can conclude everything in the lower part. So suppose we have statements S1 = s := s + i; and S2 = i := i+1; and suppose we want to know the precondition {P} that, when true before BOTH S1 and S2 guarantees {R} to be true afterward, where {R} = {s = 1/2(i)(i-1)}. We use the axiom for assignment and the rule for composition: if {R} = {s = 1/2(i)(i-1)} then {Q} = {s = 1/2(i)(i-1)}[i+1 / i] = {s = 1/2(i+1)(i+1-1)} so {Q} = {s = 1/2(i+1)(i)} so we have {Q} S2 {R}, now try for {P} S1 {Q}: if {Q} = {s = 1/2(i+1)(i)} then {P} = {s = 1/2(i+1)(i)}[s+i / s] = {s+i = 1/2(i+1)(i)} = {s = 1/2(i+1)(i) - i} = {s = 1/2((i+1)(i) - 2*i)} = {s = 1/2((i^2 + i - 2*i)} = {s = 1/2(i^2 - i)} so {P} = {s = 1/2(i)(i-1)} = {R}! We have now established both of ({P} S1 {Q}) and ({Q} S2 {R}). Notice that this is the upper part of the composition rule, so we can conclude the lower part: {P} S1 ; S2 {R} But notice also that in deriving {Q} and then {P}, we discovered that {P} = {R}, so we can in fact conclude that {P} S1 ; S2 {P} If S1 and S2 comprised the body of a while loop, we would have discovered that {P} is a LOOP INVARIANT, an assertion that is true both before and after executing the loop body, and will therefore be true after the last execution of the loop, no matter how many times the loop did execute. So if our loop were: while B do (S, where S=S1;S2, example above) and if we now know: {P} S {P} we MIGHT want to conclude: {P} while B do S {P} However, we may have to account for the loop test: since the test is performed each trip through the loop, we must be sure that its being true does not interfere with the truth of {P}. So we may need to know: {P & B} S {P}. The truth of both B and P may actually be NECESSARY in order for {P} to be true at the end of the loop body. This is the case in the example below. Or the loop test may be essentially independent of P, and it may suffice to show that {P} S {P}, which is fine. Also, we do know a little more than just {P} after the loop: Since the loop terminated, we know that B must be false. So we can conclude that after the loop, {P & not B} holds. The formal rule of inference for while loops accounts for all these things: {P & B} S {P} -------------------------------- {P} while B do S {P & not B} So consider an example from lecture, the linear search algorithm. {P1} S1 A[0] := x; S2 i := n; while x <> A[i] do begin S3 i := i-1 end; {P2} return(i) First compare the while loop with the rule of inference for while loops. The loop test B is x<>A[i]. So the assertion {P & not B} from the rule corresponds to {P2} near the end of the code - we want to discover {P2}. (The loop body S is just statement S3, "i := i-1") Ok, how do we discover {P2}? It should be something that asserts the correctness of the algorithm. Then, given {P2}, we want to derive {P1} - the preconditions that must hold in order to guarantee the correctness of the algorithm. Well, if the algorithm is correct, then the value returned is the first i <= n such that x = A[i]. Therefore, x <> A[j], where i+1 <= j <= n. Also, x = A[0]; this guarantees termination, and we include this fact although we will not deal with termination formally. So, {P2} = {x=A[0] & x <> A[i+1, i+2, ... , n] & x=A[i]} ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^ P not B The inference rule says, if we prove that {P} will hold after S whenever {P & B} holds before S, --------------- THEN WE CAN CONCLUDE THAT ------------------ {P & not B} will hold after the the while loop, whenever {P} holds before the while loop. Well, S is just the statement: i := i-1; P is the assertion {x=A[0] & x <> A[i+1, i+2, ... , n]}, and B is the assertion {x <> A[i]} so, by the assignment axiom, {P}[i-1 / i] i := i-1; {P} {P}[i-1 / i] = {x=A[0] & x <> A[i+1, i+2, ... , n]}[i-1 / i] = {x=A[0] & x <> A[i-1+1, i-1+2, ... , n]} = {x=A[0] & x <> A[i, i+1, ... , n]} = {x=A[0] & x <> A[i] & x <> A[i+1, ... , n]} = {x=A[0] & x <> A[i+1, ... , n] & x <> A[i]} ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^ so {P}[i-1 / i] = {P} & B So we have shown that: {P & B} S {P} But by the rule for the while loop, we now know: {P} while B do S {P & not B} OK, so if {P} holds before the while loop, then {P & not B} = {P2} holds at the end of the algorithm, which asserts the correctness of the algorithm. Now we need to find the preconditions {P1} that must hold at the beginning of the algorithm, in order for {P} to hold just prior to the while loop. This part is easy using the assignment axiom! We just take {P} and back it up through statements S2 and S1. We know that {P}[n / i] i := n; {P} and {P}[n / i] = {x=A[0] & x <> A[i+1, i+2, ... , n]}[n / i] = {x=A[0] & x <> A[n+1, ... , n]} = {x=A[0] & true} = {x=A[0]} (call this {Q}) Also (assignment axiom again) we know that: {Q}[x / A[0]] A[0] := x; {Q} {x=A[0]}[x / A[0]] A[0] := x; {x=A[0]} and {x=A[0]}[x / A[0]] = {x = x} = {true} (but this is {P1}!!) Now we have shown that: {true} S1 {Q} and {Q} S2 {P} So by the composition rule we have: {true} S1; S2 {P} And now to be absolutely thorough, we have: {true} (S1; S2) {P} and {P} while B do S {P & not B} -------------------------------------------------------------- {true} (S1; S2); while B do S {P & not B} from the composition rule. This states that the precondition required at the beginning of the algorithm for guaranteeing the postcondition (correctness) {P & not B} is simply that {true} holds! But {true} ALWAYS holds. So the algorithm ALWAYS behaves correctly. In class, we may also discuss the rule of inference for conditional statements: Given the conditional: if B then S we would like to discover assertions P and Q such that if P holds prior to the conditional, then Q holds after the conditional, i.e., {P} if B then S {Q} But S will execute exactly when B holds, so we need to establish {P & B} S {Q} But if (not B) holds, S does not execute, so we also need to establish {P & not B} ==> {Q} The formal rule of inference for conditionals accounts for all these things: {P & B} S {Q}, {P & not B} ==> {Q} ------------------------------------- {P} if B then S {Q} We need this rule (and the WHILE axiom) for the following example in which we prove the correctness of a routine for finding the maximum value in an array. Here, all the assertions are filled in between the lines of code. To actually do the proof, start with the assertion of correctness at the end of the algorithm (P /\ ~Bloop) and establish step by step the assertion required at the beginning. Fortunately, the assertion needed at the beginning is merely {A[1]=A[1]} = {true}. 1) {A[1]=A[1]} = {true} 2) x := A[1]; 3) {j<2 ==< x=max(A[1], ...A[j])} = {x=A[1]} 4) i := 2; 5) {j<i ==< x=max(A[1], ...A[j])} = P 6) while (i <= N) (i <= N) = Bloop 7) {j<i ==< x=max(A[1], ...A[j])} = P 8) if (A[i] < x) then (A[i] < x) = Bcond x := A[i]; 9) {j<i+1 ==< x=max(A[1], ...A[j])} = P' 10) i := i+1; 11) {j<i ==< x=max(A[1], ...A[j])} = P 12) end while 13) {j<i ==< x=max(A[1], ...A[j])} = P /\ {i<N} = ~Bloop Note that (P /\ ~Bloop) ==< x=max(A[1], ...A[N]) = correctness! You should be able to start with the assertions at 13) and, under the assumption that the premise of the while axiom holds, know that assertion P at 5) guarentees 13). Then from 5) we can, using the assignment axiom, conclude that 3), and then 1) are the necessary conditions at those points in the program. Showing that P is indeed a loop invariant of the while loop is difficult and requires using the if-then axiom and a bit of clever reasoning. This may be shown in class, but is more difficult than what will be expected of students.


  • Notes on Quicksort in Lisp ; ************************************************************************** ; * The first version of quicksort below is commented out. In that * ; * version, the function "partition" is written with 4 parameters. The * ; * last two parameters are the two lists that are (eventually) the two * ; * partitions built. In effect, we use extra formal parameters to store * ; * partial results as the recursion proceeds. In the base case of the * ; * recursion, there are no more elements in L to be added to the lists of * ; * the partition, so we just return a list whose elements are GT & LTE. * ; * The second version has some commented code, but the uncommented code * ; * constitutes a complete running version of quicksort. The commented * ; * code is what one might write as a first pass - whenever an object is * ; * needed, we just write down the LISP expression that will produce it. * ; * Then, we observe that certain computations are being performed redun- * ; * dantly, and we pass those as actual parameters to auxiliary functions. * ; ************************************************************************** ; ;(defun quicksort (L) ; (cond ((null L) nil) ; (t (combine (car L) ; (partition (car L) (cdr L) nil nil) )) ) ) ;(defun combine (split L) ; (append (quicksort (car L)) ; (cons split (quicksort (cadr L))) ) ) ;(defun partition (split L GT LTE) ; (cond ((null L) (list GT LTE)) ; ((> (car L) split) ; (partition split (cdr L) (cons (car L) GT) LTE) ) ; (t (partition split (cdr L) GT (cons (car L) LTE))) ) ) (defun quicksort (L) (cond ((null L) nil) ; (t (append (quicksort (car (partition (car L) (cdr L))) ; (cons (car L) ; (quicksort ; (cadr (partition (car L) (cdr L))) ) ) ))) ) ) (t (combine (car L) (partition (car L) (cdr L)))) ) ) (defun combine (split part) (append (quicksort (car part)) (cons split (quicksort (cadr part))) ) ) (defun partition (sp l) (cond ((null l) (list nil nil)) ; ((> (car l) sp) ; (list (cons (car l) ; (car (partition sp (cdr l))) ) ; (cadr (partition sp (cdr l))) ) ) ; (t (list (car (partition sp (cdr l))) ; (cons (car l) ; (cadr (partition sp (cdr l))) ) )) ) ) (t (build sp (car l) (partition sp (cdr l)) )) ) ) (defun build (split carL part) (cond ((> carL split) (list (cons carL (car part)) (cadr part) ) ) (t (list (car part) (cons carL (cadr part)) ) ) ) )

  • Notes on Functional Programming
    Functional Programming
    How do we handle * Sequences of actions * Storing intermediate results * Avoiding unnecessary computation Example: Define a function "pfacts" that takes a list L of numeric atoms as argument, and 1. Computes the sorted version of L 2. Computes the max and min of L 3. Prints L, the sorted version of L, max, and min.
    (defun pfacts1 (L) (setf SL (isort L)) (setf BIG (car SL)) (setf SMALL (lastel SL)) (print L) (terpri) (print SL) (terpri) (print BIG) (princ " ") (princ SMALL) T) Imperative Style Problems - destructive assignment - SL, BIG, SMALL, are global
    (defun pfacts2 (L) (let* ((SL (isort L)) (BIG (car SL)) (SMALL (lastel SL)) ) (print L) (terpri) (print SL) (terpri) (print BIG) (princ " ") (princ SMALL) T) ) Imperative Style, but SL, BIG, SMALL, are local
    (defun pfacts3 (L) (print L) (terpri) (print (isort L)) (terpri) (print (car (isort L))) (princ " ") (princ (lastel (isort L)))) T) ) Functional Style Problem - sorted the list 3 times!
    (defun pfacts4 (L) (aux L (isort L))) (defun aux (L SL) (print L) (terpri) (print SL) (terpri) (print (car SL)) (princ " ") (princ (lastel SL)) T) Functional Style and Efficient
    (defun lastel (L) (cond ((null (cdr L)) (car L)) (t (lastel (cdr L))) ) ) (defun isort (L) (cond ((or (null L) (null (cdr L))) L) (t (place (car L) (isort (cdr L)) )) ) ) (defun place (e L) (cond ((null L) (list e)) ((<= e (car L)) (cons e L)) (t (cons (car L) (place e (cdr L)) )) ) )
    QUICKSORT (functionally) (defun partition (sp l) (cond ((null l) (list nil nil)) ; ((> (car l) sp) ; (list (cons (car l) ; (car (partition sp (cdr l))) ) ; (cadr (partition sp (cdr l))) ) ) ; (t (list (car (partition sp (cdr l))) ; (cons (car l) ; (car (partition ; sp (cdr l) )) ) )) ) ) ; ; Would have computed "(partition sp (cdr l))" 4 times! (t (build sp (car l) (partition sp (cdr l)) )) ) ) (defun build (split carL part) (cond ((> carL split) (list (cons carL (car part)) (cadr part) ) ) (t (list (car part) (cons carL (cadr part)) ) ) ) ) (defun quicksort (L) (cond ((null L) nil) ; (t (append (quicksort ; (car (partition (car L) (cdr L))) ; (cons (car L) ; (quicksort ; (cadr ; (partition ; (car L) ; (cdr L) ) ) ) ) ))) ) ) ; ; Would have computed "(partition (car L) (cdr L)))" twice! (t (combine (car L) (partition (car L) (cdr L)) )) ) ) (defun combine (split part) (append (quicksort (car part)) (cons split (quicksort (cadr part)) ) ) )
  • Notes on Prolog program Family Family Relationships john, jim, and jerry are males, while sue, carol, and alice are females. jerry and carol are john and alice's offspring, ted is jerry and sue's offspring, and bob is jim and carol's offspring. 1) male(john). 2) male(jim). 3) male(jerry). 4) female(sue). 5) female(carol). 6) female(alice). 7) parent(john, jerry). 8) parent(john, carol). 9) parent(alice, jerry). 10) parent(alice, carol). 11) parent(sue, ted). 12) parent(jerry, ted). 13) parent(carol, bob). 14) parent(jim, bob). We store the 14 assertions in the file ``family.pro''. pl Welcome to SWI-Prolog (Version 3.1.2) Copyright (c) 1993-1998 University of Amsterdam. All rights reserved. | ?- consult('family.pro'). ( or ['family.pro']. ) < message, possible errors, etc. > yes | ?- listing. < lists all rules in the system > | ?- listing(parent). < lists all parent rules in the system > | ?- parent(sue, ted). yes | ?- parent(alice, X). X = jerry < waits for response, ; means more > X = carol < ; > no | ?- parent(Y, carol). Y = john < ; > Y = alice < ; > no | ?- male(X). 1) male(john). X = john ; 2) male(jim). X = jim 3) male(jerry). yes 4) female(sue). | ?- male(bob) 5) female(carol). no 6) female(alice). | ?- 7) parent(john, jerry). 8) parent(john, carol). 9) parent(alice, jerry). 10 parent(alice, carol). 11 parent(sue, ted). 12 parent(jerry, ted). 13 parent(carol, bob). 14 parent(jim, bob). 15) sib(X, Y) :- parent(Z, X), parent(Z, Y) X \== Y. 16) brother(X, Y) :- sib(X, Y), male(X) Let's try to solve the goal | ?- brother(jerry, carol). RULE 16 {jerry/X, carol/Y} | ?- sib(jerry, carol), RULE 15 male(jerry). {jerry/X, carol/Y} | ?- parent(Z, jerry), RULE 7 {john/Z} parent(Z, carol), jerry \== carol, male(jerry). | ?- parent(john, carol), RULE 8 jerry \== carol, male(jerry). | ?- jerry \== carol, male jerry). \==, RULE 3 yes DONE!!
  • Notes on Unification ----------------------- U N I F I C A T I O N ----------------------- Variables: symbol strings beginning with upper-case characters: E.g., V3 Uabc Aaa, A8xv, F, Kx17tr, ... Constants: symbol strings beginning with lower-case characters: E.g., c1, k, fx, t17, xX27dV terms: t1 = p(a, b, f(q,Y), f(a,Z), W) t2 = p(V, b, X, f(W,Z), W) t3 = p(V, b, c, f(a,c), W) t4 = p(X, b, c, f(X,V), W) t5 = p(f(a, W), b, c, f(V,X), V) t6 = m(X, g(X,X,X), V, ) t7 = m(g(Y,Y,Y), Z, g(Z,Z,Z)) The DIFFERENCE, diff(A, B), of two expressions A and B is defined to be their first two unequal subexpressions (scanning left to right). The difference of h(a, b, f(q,Y), f(a,Z), W) and h(X, b, c, f(X,V), W) is The difference of h(a, b, f(q,Y), f(a,Z), W) and h(a, b, c, f(X,V), W) is The difference of (a, b, c, f(V,X), W) and (a, b, c, f(V,Y), W) is The difference of (a, b, c, f(X,V), W) and (a, b, c, f(X,V), W) is A difference is REDUCIBLE if one of the subexpressions is a variable not occurring in the other. UNIFYING expressions: can we unify t1 and t2? (Can we make them identical?) t1 = '(a, b, f(q, Y) f(a, Z), W) t2 = '(V, b, X, f(W, Z), W) V becomes a and X becomes f(q, Y) and W becomes a The substitution of expressions for variables is represented as a set of COMPONENTS. We can write each component as a pointer from the variable to the expression (typical implementation). The variable is bound to the expression; i.e., the expression replaces the variable. The substitution above would be represented as: {V --> a, X --> f(q, Y), W --> a} U N I F I C A T I O N (informal algorithm) To unify expressions A and B, we start with A, B, diff(A, B), and the empty substitution S = {}. While diff(A, B) is reducible: Modify A and B as indicated by (diff A B); Augment substitution S as indicated by (diff A B); Compute a new (diff A B). EndWhile EXAMPLE ----------------------------------------------------- A = p(X, f(g(Y)), f(X )) B = p(h(Y, Z), f(Z ), f(h(U, V))) S = {} (diff A B) = ----------------------------------------------------- A = p(h(Y, Z), f(g(Y)), f(h(Y, Z))) B = p(h(Y, Z), f(Z ), f(h(U, V))) S = {X --> h(Y,Z)} (diff A B) = ----------------------------------------------------- A = p(h(Y, g(Y)), f(g(Y)), f(h(Y, g(Y)))) B = p(h(Y, g(Y)), f(g(Y)), f(h(U, V, ))) S = {X --> h(Y, g(Y)), Z --> g(Y)} (diff A B) = ----------------------------------------------------- A = p(h(Y, g(Y)), f(g(Y)), f(h(Y, g(Y)))) B = p(h(Y, g(Y)), f(g(Y)), f(h(Y, V ))) S = {X --> h(Y, g(Y)), Z --> g(Y), U --> Y} (diff A B) = ----------------------------------------------------- A = p(h(Y, g(Y)), f(g(Y)), f(h(Y, g(Y)))) B = p(h(Y, g(Y)), f(g(Y)), f(h(Y, g(Y)))) S = {X --> h(Y, g(Y)), Z --> g(Y), U --> Y, V --> g(Y)} (diff A B) = ----------------------------------------------------- MATCHING & LIST NOTATION We could choose "dot" as a binary function to take the place of "." in LISP. We could choose "nil" as a constant to take the place of NIL, or (), in LISP. Then [X | T] is syntactic sugar for dot(X,T), Similarly, [a, b, c] is syntactic sugar for dot(a, dot(b, dot(c, nil))) and [] is syntactic sugar for nil. When Prolog matches [X | T] with [a, b, c], the binding is {X --> a, T --> [b, c]} But ask Prolog to match dot(X,T) with dot(a, dot(b, dot(c, nil))) The binding is {X --> a, T --> dot(b, dot(c, nil))} But this is STILL {X --> a, T --> [b, c]}
  • Notes on Cut in Prolog ************************************************************************** * The CUT ("!") in Prolog (and other stuff) * ************************************************************************** 1. maximum(X, Y, X) :- X >= Y. 2. maximum(X, Y, Y) :- X < Y. ?- maximum(5, 4, M), even(M). [1] {5/X1, 4/Y1, 5/M} ?- 5 >= 4, even(5). [] {} ?- even(5). [] {} FAIL ?- 5 >= 4, even(5). [] {} FAIL (no other choice) ?- maximum(5, 4, M), even(M). [2] {5/X2, 4/Y2, 4/M} ?- 5 < 4, even(4). [] {} FAIL ?- maximum(5, 4, M), even(M). FAIL (no other choice) The Cut "!" (controlling control) 3. fastmax(X, Y, X) :- X >= Y, ! . 4. fastmax(X, Y, Y) :- X < Y. ?- fastmax(5, 4, M), even(M). [3] {5/X1, 4/Y1, 5/M} ?- 5 >= 4, !, even(5). [] {} ?- !, even(5). [] {} Succeed, but make no other attempt to solve 5 >= 4 or fastmax(5, 4, M) ?- even(5). [] {} FAIL (no further attempts) Cut considered harmful 5. fastermax(X, Y, X) :- X >= Y, ! . 6. fastermax(X, Y, Y). ?- fastermax(5, 4, M), even(M). FAIL (just as fastmax) ?- fastermax(5, 4, 4), even(4). [5] Can't! (No match) Cut never activated. ?- fastermax(5, 4, 4), even(4). [6] {5/X1, 4/Y1} ?- even(4). [] {} ?- (empty goal list) SUCCESS !!! "Cut"ting out Redundancy 1) parent(sue, ted). 2) parent(john, fred). 5) parent(alice, fred). 3) parent(john, jerry). 6) parent(alice, jerry). 4) parent(john, carol). 7) parent(alice, carol). 8) parent(jerry, ted). 9) parent(carol, bob). 10) parent(jim, bob). sib(X, Y) :- parent(Z, X), parent(Z, Y), X \== Y. ?- sib(X, Y). --- gives 12 answers. from solving 1st parent goal with assertion parent(john, fred). then solving the 2nd parent goal with assertion parent(john, jerry). and then re-solving the 2nd parent goal with parent(john, carol). We get X=fred Y=jerry, and X=fred Y=carol 2 more solving 1st goal with parent(john, jerry) - 2 more solving 1st goal with parent(john, carol) - and then another 6 due to parent(alice, ____). Now an attempt to use cut to remove the redundancy 1) parent(sue, ted). 2) parent(john, fred). 5) parent(alice, fred). 3) parent(john, jerry). 6) parent(alice, jerry). 4) parent(john, carol). 7) parent(alice, carol). 8) parent(jerry, ted). 9) parent(carol, bob). 10) parent(jim, bob). sib1(X,Y) :- parent(Z,X), !, parent(Z,Y), X \== Y. ?- sib1(X,Y). --- fails. 1st parent goal frozen on assertion 1). sib2(X, Y) :- parent(Z, X), findfirstsib(Y,Z,X). findfirstsib(Y,Z,X) :- parent(Z,Y), !, X \== Y. ?- sib2(X,Y). --- 4 solutions - two non-redundant solutions each for parents john & alice The cut is freezing the choice for Y in parent(Z,Y) to be the very first for which Z is the parent. Backtracking allows other choices for X in parent(Z,X) To get rid of the redundant solutions - one for each parent - use only the female parent. sib3(X,Y) :- parent(Z,X), female(Z), fsib(Y,Z,X). fsib(Y,Z,X) :- parent(Z,Y), !, X == Y. ?- sib3(X,Y). --- only 2 solutions: X=jerry, Y=fred and X=carol, Y=fred But, suppose the goal is: ?- sib3(fred, Y). --- Prolog responds NO! Problem: The choice for 1st arg. in sib3 is now fixed as fred. But the choice for the 2nd arg. is frozen on fred also, by the cut. Solution: sib4(X,Y) :- var(X), !, sib3(X,Y); sib3(Y,X). Negation as Failure 1) animal(dog). 2) animal(cat). 3) animal(dove). 4) animal(python). 5) snake(python). 6) likes(mary, X) :- animal(X). ?- likes(mary, python). Yes Can we express that mary likes all animals EXCEPT SNAKES? 6) likes(mary, X) :- animal(X), notsnake(X). 6a) notsnake(X) :- snake(X), !, fail. 6b) notsnake(X) :- true. We can use ";" to indicate an alternative: (shorthand for 2 rules with the same head) 6ab) notsnake(X) :- snake(X), !, fail; true. Define "not P" to mean: not P succeeds if P fails, and not P fails if P succeeds. not P :- P, !, fail. not P :- true. SWI-Prolog has "not" built-in as a prefix op. Instead of likes(mary, X) :- animal(X), notsnake(X). We can write likes(mary, X) :- animal(X), not snake(X). and the rules for "notsnake" are no longer necessary. Goals with variables 1) odd(7). 3) integer(2). 2) prime(2). 4) integer(7). 5) even(X) :- not odd(X). Goals containing variables can produce incorrect results with negation and/or cut. We ask for an integer that is even & prime ?- integer(X), even(X), prime(X). X = 2 yes Now ask in the other order: ?- even(X), prime(X), integer(X). no ( Why ? ) Solving "integer(X)" bound X to a constant, so "even(2)" was the goal invoking rule 5. But invoking rule 5 with "even(X)" allowed a successful attempt to prove "odd(X)", leading to failure. Computing with several solutions 1) animal(dog). 5) animal(dove). 2) animal(cat). 6) two_legs(dove). 3) four_legs(dog). 7) animal(python). 4) four_legs(cat). 8) snake(python). 8) likes(mary, X) :- animal(X), not snake(X). ?- likes(mary, X). X = dog; X = cat; X = dove; no Can we get the list of animals mary likes? ?- setof(X, likes(mary, X), L). X = _0 L = [dog, cat, dove]. How about the quadripeds mary likes? 9) like_4(X,Y) :- four_legs(Y), likes(X,Y). ?- setof(Y, like_4(X,Y), L). X = mary Y = _0 L = [dog, cat]; no
  • Notes on Depth-First Search in Prolog Depth-First Search Suppose we are given some initial state, i.e., we have an assertion: initstate(). We also have a final state assertion: finalstate(). Finally, we have rules that determine for any given state S, any possible state S1 directly reachable from S. So we can solve goals of the form: next_state(S, S1) Rules for Depth-First Search dfsearch(AnsPath) :- initial_state(Init), df([Init], AnsPath). df([S | Path], [S]) :- final_state(S), !. df([S | Path], [S | AnsPath]) :- extend([S | Path], S1), df([S1, S | Path], AnsPath). extend([S | Path], S1) :- next_state(S, S1), not(member(S1, [S | Path])). member(S, [S | Rest]). member(S, [_ | Rest]) :- member(S, Rest). Assume we are allowed 3 piles of blocks, and there are 3 blocks called a, b, and c. Initially the three piles are: ___ | b | |___| ___ ___ | c | | a | | | | | _______________________ 1 2 3 So our initial state can be represented by the term piles([b,c], [a], []) We are to write rules that express possible next states for any given state. By adding such rules to the rules for depth-first search, we will have a program that can discover a means of going from the initial state to the final state, if that is possible. The initial and final states: initial_state(piles([b,c], [a], [])). final_state(piles([], [a,b,c], [])). Rules for next_state next_state(Piles, NewPiles) :- choose_indices(I,J), move_block(I, J, Piles, NewPiles). choose_indices(I, J) :- num_of_piles(N), index(I, N), index(J, N), I \== J. num_of_piles(3). index(1, N). index(I, N) :- index(K, N), ( (K >= N, !, fail); (K < N, I is K+1) ). move_block(I, J, Piles, NewPiles) :- arg(I, Piles, [B|IBlocks]), arg(J, Piles, JBlocks), argrep(Piles, I, IBlocks, IPiles), argrep(IPiles, J, [B|JBlocks], NewPiles). argrep(piles(One,Two,Three), 1, N1, piles(N1,Two,Three)). argrep(piles(One,Two,Three), 2, N2, piles(One,N2,Three)). argrep(piles(One,Two,Three), 3, N3, piles(One,Two,N3)). | ?- ['dfs.blocks']. dfs.blocks consulted 2016 bytes 0.166667 sec. yes | ?- dfsearch(Answer). Answer = [piles([b,c],[a],[]), piles([c],[b,a],[]), piles([],[c,b,a],[]), piles([],[b,a],[c]), piles([b],[a],[c]), piles([],[a],[b,c]), piles([a],[],[b,c]), piles([b,a],[],[c]), piles([a],[b],[c]), piles([],[a,b],[c]), piles([],[b],[a,c]), piles([b],[],[a,c]), piles([a,b],[],[c]), piles([c,a,b],[],[]), piles([a,b],[c],[]), piles([b],[a,c],[]), piles([],[b,a,c],[]), piles([],[a,c],[b]), piles([a],[c],[b]), piles([],[c],[a,b]), piles([c],[],[a,b]), piles([a,c],[],[b]), piles([c],[a],[b]), piles([],[c,a],[b]), piles([],[a],[c,b]), piles([a],[],[c,b]), piles([c,a],[],[b]), piles([b,c,a],[],[]), piles([c,a],[b],[]), piles([a],[c,b],[]), piles([],[a,c,b],[]), piles([],[c,b],[a]), piles([c],[b],[a]), piles([],[b],[c,a]), piles([b],[],[c,a]), piles([c,b],[],[a]), piles([b],[c],[a]), piles([],[b,c],[a]), piles([a],[b,c],[]), piles([],[a,b,c],[])] yes | ?- problem state: piles(Pile1, ..., Pilen) It might be better to report a sequence of moves, rather than a sequence of states. We now define a problem state to be the pair: [move(I,J), piles(Pile1, ..., Pilen)] Check membership only with respect to states, not with respect to moves: dfsearch(AnsPath) :- initial_state(Init), df([Init], AnsPath). df([S | Path], [S]) :- final_state(S), !. df([S | Path], [S | AnsPath]) :- extend([S | Path], S1), df([S1, S | Path], AnsPath). extend([S | Path], S1) :- next_state(S, S1), not(member_state(S1, [S | Path])). member_state([_, S], [[_, S] | Rest]). member_state(X, [_|Rest]) :- member_state(X, Rest). initial_state([none, piles([b,c], [a], [])]). final_state([_, piles([], [a,b,c], [])]). num_of_piles(3). next_state([_, Piles], [move(I,J), NewPiles]) :- choose_indices(I,J), move_block(I, J, Piles, NewPiles). choose_indices(I, J) :- num_of_piles(N), index(I, N), index(J, N), I \== J. index(1, N). index(I, N) :- index(K, N), ( (K >= N, !, fail); (K < N, I is K+1) ). move_block(I, J, Piles, NewPiles) :- arg(I, Piles, [B|IBlocks]), arg(J, Piles, JBlocks), argrep(Piles, I, IBlocks, IPiles), argrep(IPiles, J, [B|JBlocks], NewPiles). argrep(piles(One,Two,Three), 1, N1, piles(N1,Two,Three)). argrep(piles(One,Two,Three), 2, N2, piles(One,N2,Three)). argrep(piles(One,Two,Three), 3, N3, piles(One,Two,N3)). | ?- ['dfs.blocks.moves']. dfs.blocks.moves consulted 2212 bytes 0.216667 sec. yes | ?- dfsearch(Answer). Answer = [[none,piles([b,c],[a],[])], [move(1,2),piles([c],[b,a],[])], [move(1,2),piles([],[c,b,a],[])], [move(2,3),piles([],[b,a],[c])], [move(2,1),piles([b],[a],[c])], [move(1,3),piles([],[a],[b,c])], [move(2,1),piles([a],[],[b,c])], [move(3,1),piles([b,a],[],[c])], [move(1,2),piles([a],[b],[c])], [move(1,2),piles([],[a,b],[c])], [move(2,3),piles([],[b],[a,c])], [move(2,1),piles([b],[],[a,c])], [move(3,1),piles([a,b],[],[c])], [move(3,1),piles([c,a,b],[],[])], [move(1,2),piles([a,b],[c],[])], [move(1,2),piles([b],[a,c],[])], [move(1,2),piles([],[b,a,c],[])], [move(2,3),piles([],[a,c],[b])], [move(2,1),piles([a],[c],[b])], [move(1,3),piles([],[c],[a,b])], [move(2,1),piles([c],[],[a,b])], [move(3,1),piles([a,c],[],[b])], [move(1,2),piles([c],[a],[b])], [move(1,2),piles([],[c,a],[b])], [move(2,3),piles([],[a],[c,b])], [move(2,1),piles([a],[],[c,b])], [move(3,1),piles([c,a],[],[b])], [move(3,1),piles([b,c,a],[],[])], [move(1,2),piles([c,a],[b],[])], [move(1,2),piles([a],[c,b],[])], [move(1,2),piles([],[a,c,b],[])], [move(2,3),piles([],[c,b],[a])], [move(2,1),piles([c],[b],[a])], [move(1,3),piles([],[b],[c,a])], [move(2,1),piles([b],[],[c,a])], [move(3,1),piles([c,b],[],[a])], [move(1,2),piles([b],[c],[a])], [move(1,2),piles([],[b,c],[a])], [move(3,1),piles([a],[b,c],[])], [move(1,2),piles([],[a,b,c],[])]] yes

  • Return Back to CSI 311 Home Page