Class 37: Discussion of Exam 3
Held Monday, May 3
- Exam 3 has been graded.
- The median grade was 88.5.
- The average grade was 88.
- The first two questions were hard; you should be pround that
you did so well as a class.
- In spite of the good outcomes, we'll still spend a little time
on the exam today.
- I'll do my best to have the final ready early next week.
- This problem had a hidden similarity to the next problem;
just as it's bad to have ambiguous grammars because they
give multiple proofs of the same thing, it's also bad to
have Prolog programs with overlapping patterns, since they,
too, can give too many proofs (sometimes of the wrong thing).
- I typically did not take off if you had overlapping patterns
(and the overlap did not cause problems). However, it is
something you should avoid.
- A related problem is not making sure that you had sufficiently
many patterns. For example, many of you could not do anything
?- pred(pred(succ(succ(succ(zero)))), succ(zero))
- How should you make sure your patterns are complete? Typically
with some careful thought.
- Note that there was a hidden agenda in the problem: making you
think about prolog as a model of computation, and not just a
way of representing predicate logic.
- A number of you had problems writing ``X and Y have the same
canonical form'' (which is one possible definition of
- This is wrong:
Why? Because there is an unnecessary (and potentially harmful)
recursive call to
equals(X,Y) :- canonical(X,XC), canonical(Y,YC), equals(XC,YC).
- This is better. It uses a separate predicate to ensure that the
two canonical forms are the same.
equals(X,Y) :- canonical(X,XC), canonical(Y,YC), same(XC,YC).
- This is best.
equals(X,Y) :- canonical(X,C), canonical(Y,C).
- Wyatt had a particularly elegant implementation of the
- ``Move'' all of the operators from the first argument to the
second argument (this involves inverting them).
equals(pred(X),Y) :- equals(X,succ(Y)).
equals(succ(X),Y) :- equals(X,pred(Y)).
- We're now in the form
switch to a separate predicate.
equals(zero,Y) :- balance(zero,Y).
- We now treat the two arguments as something like stacks, repeatedly
popping things off the second argument and putting them on top
of the first argument. When both arguments have the same top,
we pop both. We're done when we reach zero in both stacks.
Note that I've used nonoverlapping patterns that handle every
/* 0 == 0 */
/* X-1 == Y-1 iff X = Y */
balance(pred(X),pred(Y)) :- balance(X,Y).
/* X+1 == Y+1 iff X = Y */
balance(succ(X),succ(Y)) :- balance(X,Y).
/* X+1 == Y-1 iff X+2 = Y */
balance(succ(X),pred(Y)) :- balance(succ(succ(X)), Y).
/* X-1 == Y+1 iff X-2 = Y */
balance(pred(X),succ(Y)) :- balance(pred(pred(X)), Y).
/* 0 == Y-1 iff Y == - */
balance(zero,pred(Y)) :- balance(succ(zero), Y).
/* 0 == Y+1 iff Y == -1 */
balance(zero,succ(Y)) :- balance(pred(zero), Y).
- My version is wyatt.pro.
- Question 2A (the ambiguous parenthetical grammar) had a hidden
agenda: getting you to think about grammars which can generate
- In particular, the original grammar is not just ambiguous because
you can left-associate and right-associate when you have three
sets of parentheses, it's also ambiguous because you can
add as many extra epsilons as you'd like.
- S => (S) => ()
- S => SS => (S)S => ()S => ()
- S => SS => S(S) =gt; S() => ()
- S => SS => SSS => ...
- Too many of you, in making the grammar unambiguous, also eliminated
strings from the language. That's not reasonable.
The most commonly eliminated strings were
- This was another case in which many of you did not think about all
the implications. In particular, some didn't allow close braces.
(()(()] should be in both of the first
languages, but some of you didn't accept it.
- Repeat after me: assignability is not equivalence.
- Few of you thought about recursive calls to your predicate.
- In spite of its many advantages, call-by-name is not used in a large
number of languages.
- Why not?
- Primarily because it's difficult to implement efficiently.
- Often, you need to pass around ``code to execute''.
- This code may need to be passed around at run time.
- It becomes surprisingly difficult to implement
which swaps the values of two variables.
- Why is it difficult? Consider
- If we implement
tmp = x;
x = y;
y = tmp;
- Then the call to
tmp = i;
i = A[i];
A[i] = tmp;
- We're referring to a different index!
- As far as I know, there is no general way around this problem
(other than restricting how people use swap).
- Many algorithms also need a way to express repetition. It
is possible, but not elegant, to express repetition with conditional
- If a language does not provide some form of
repetition, then it is not Turing complete.
- Recursion plus conditionals provide one form of repetition.
- A particularly common form of repetition is the loop, which
repeats a statement some number of times.
- Loops provide an abstraction of repetition that can help ensure that
- We can only enter the repeated statements in one (or a few ways).
- We can only have one subsequent statement (one that follows the
- There are only a limited number of ways to exit the loop.
- Repetition also introduce some problems into the language. Once programs
can repeat parts, it becomes difficult (if not impossible) to prove that
a program terminates.
- There are two general kinds of loops:
for loops traditionally repeat a statement (or block
of statements) a fixed number of times.
while loops repeat a statement (block of statements)
an arbitrary number of times.
- For loops generally express a fixed number of repetitions
using a counter variable.
- For loops generally contain five components:
- A counter variable
- A starting value for the counter variable
- An ending value for the counter variable
- An increment to the counter variable
- A statement to execute
- One advantage of such loops is that they are easy to analyze at compile
- If the starting value, ending value, and counter are constants,
then we can know at compile time how many times the loop will repeat.
- We can unroll such a loop so that we never have to do a
test (or even a conditional jump).
- We can guarantee that the loop terminates.
- Some variants of for loops (e.g., those in Java and C) these
allow much more general components.
- For example, many
languages permit a termination condition rather than an ending
- Use of termination conditions obviate the aforementioned benefits.
- For loops also provide a natural way to express a number of algorithms.
- Even without the "extensions" of C and Java, there are a number of
design issues in for loops.
- The counter.
- What types are valid for counters?
- Can counters be accessed within the loop? (Usually yes.)
- Can counters be changed within the loop? (Usually no.)
- Is it defined after the loop exits? If so, how? (Usually
- The bounds.
- Are we restricted to constants, or can we use expressions?
- If we can use expressions, how often are they evaluated? (Only
at the beginning of the loop? Each time through? Something else?)
- Can we specify both bounds? Some loops only allow you to specify the
upper bound and start with 1 or 0.
- The body.
- Can one exit the loop from within the body?
- Is the body executed if the lower bound is "after" the upper bound?
- There are also a number of variants of for loops. One typical one
is the foreach loop, which steps through the elements in a
- A related control structure is the iterator which is used
to step through elements in a data structure.
- While loops are loops that execute their body until a condition is
reached (rather than a fixed number of steps).
- They are more general than for loops, but the number of repetitions
usually cannot be predicted in advance.
- In fact, it is impossible to write a program that can read any
loop and determine whether the loop terminates. (This is the
the halting problem.)
- However, it is often possible to prove that particular loops
- As in each of the structures we have examined, there are many design
decisions. For example,
- Do you execute the body before or after the test?
- If you execute the test first, and it fails, do you still execute the body?
- Can you exit prematurely?
- Because of some of the analysis problems, it is particularly important
to carefully write loops using good design. This includes
- Preconditions: what you know before you enter the loop (or any
statement within the loop)
- Postconditions: what you know after you leave the loop
- Loop invariants: some fact the loop maintains.
- Dijkstra's guarded loops generalize the guarded conditional to provide
- As with conditionals, guarded loops contain a number of guard/statement
- If no guards hold, the loop terminates.
- If one guard holds, the corresponding statement is executed.
- If one of the guards hold, one of the corresponding statements
- Created Tuesday, January 19, 1999 as a blank outline.
- Filled in the details on Sunday, May 2, 1999. Most were taken
from the previous two outlines.