Programming Languages

Start Lecture #3

Chapter 6: Control Flow

Remark: We will have a midterm. It will be 1 hour, during recitation. It will not be either next monday or the monday after.

6.1: Expression Evaluation

Most languages use infix notation for built in operators. Scheme is an exception (+ 2 (/ 9 x)).

    function "*" (A, B: Vector) return Float is
      Result: Float :=0.0;
    begin
      for I in A'Range loop
        Result := Result + A(I)*B(I);
      end loop;
      return Result;
    end "*";
  

Some languages permit the programmer to extend the infix operators. An example from Ada is at right. This example assumes Vector has already been declared as a one-dimensional array of floats. A'R gives the range of the legal subscripts, i.e. the bounds of the array. With loops like this it is not possible to access the array out of bounds.

6.1.1: Precedence and Associativity

Most languages agree on associativity, but APL is strictly right to left (no precedence, either). Normal is for most binary operators to be left associative, but exponentiation is right associative (also true in math). Why?

Languages differ considerably in precedence (see figure 6.1 in 3e). The safest rule is to use parentheses unless certain.

Homework: 1. We noted in Section 6.1.1 that most binary arithmetic operators are left-associative in most programming languages. In Section 6.1.4, however, we also noted that most compilers are free to evaluate the operands of a binary operator. Are these statements contradictory? Why or why not?

6.1.2: Assignments

Assignment statements are the dominant example of side effects, where execution does more that compute values. Side effects change the behavior of subsequent statements and expressions.

References and Values

There is a distinction between the container for a value (the memory location) and the value itself. Some values, e.g., 3+4, do not have corresponding containers and, among other things, cannot appear on the left hand side of an assignment. Otherwise said 3+4 is a legal r-value, but not a legal l-value. Given an l-value, the corresponding r-value can be obtained by dereferencing. In most languages, this dereferencing is automatic. Consider

    a := a + 1;
  
the first a gives the l-value; the second the r-value.

Boxing

  begin
    a := if b < c then d else c;
    a := begin f(b); g(c) end;
    g(d);
    2+3
  end

Orthogonality

The goal of orthogonality of features is to permit the features in any combination possible. Algol 68 emphasized orthogonality. Since it was an expression-oriented language, expressions could appear almost anywhere. There was no separate notion of statement. An example appears on the right. I have heard and read that this insistence on orthogonality lead to significant implementation difficulties.

Combination Assignment Operators

I suspect you all know the C syntax

    a += 4;
  
It is no doubt often convenient, but its major significance is that in statements like
    a[f(i)] += 4;
  
it guarantees that f(i) is evaluated only once. The importance of this guarantee becomes apparent if f(i) has side effects, e.g., if f(i) modifies i or prints.

Multiway Assignment (and Tuples)

Some languages, e.g., Perl and Python, permit tuples to be assigned and returned by functions, giving rise to code such as

    a, b  :=  b, a    -- swapping w/o an explicit temporary
    x, y  :=  f(5);   -- this f takes one argument and returns two results
  

6.1.3: Initialization

Avoids the problem where an uninitialized variable has different values in different runs causing non-reproducible results. Some systems provide default initialization, for example C initializes external variables to zero.

To initialize aggregates requires a way to specify aggregate constants.

Dynamic Checks

An alternative to have the system check during execution if a variable is uninitialized. For IEEE floating point this is free, since there are invalid bit patterns (NaN) that are checked by conforming hardware. To do this in general needs special hardware or expensive software checking.

Definite Assignment

Some languages, e.g., Java and C#, specify that the compiler must be able to show that no variable is used before given a value. If the compiler cannot confirm this, a warning/error message is produced.

Constructors

Some languages, e.g., Java, C++, and C# permit the program to provide constructors that automatically initialize dynamically allocated variables.

6.1.4: Ordering within Expressions

Which addition is done first when you evaluate X+Y+Z? This is not trivial: one order might overflow or give a less precise result. For expressions such as f(x)+g(x) the evaluation order matters if the functions have side effects.

Applying Mathematical Identities

6.1.5: Short-Circuit Evaluation

Sometimes the value of the first operand determines the value of the entire binary operation. For example 0*x is 0 for any x; True or X is True for any X. Not evaluating the second operand in such cases is called a short-circuit evaluation. Note that this is not just a performance improvement; short-circuit evaluation changes the meaning of some programs: Consider 0*f(x), when f has side effects (e.g. modifying x or printing). We treat this issue in 6.4.1 (short-circuited conditions) when discussing control statements for selection.

Homework: CYU 7. What is an l-value? An r-value? CYU 11. What are the advantages of updating a variable with an assignment operator, rather than with a regular assignment in which the variable appears on both the left- and right-hand sides?

6.2: Structured and Unstructured Flow

Early languages, in particular, made heavy use of unstructured control flow, especially goto's. Much evidence was accumulated to show that the great reliance on goto's both increased the bug density and decreased the readability (these are not unrelated consequences) so today structured alternatives dominate.

A famous article (actually a letter to the editor) by Dijkstra entitled Go To Statement Considered Harmful put the case before the computing public.

Common today are

6.2.1: Structured Alternatives to goto

Common uses of goto have been captured by newer control statements. For example, Fortran had a DO loop (similar to C's for), but had no way other that GOTO to exit the loop early. C has a break statement for that purpose.

Homework: 24. Rubin [Rub87] used the following example (rewritten here in C) to argue in favor of a goto statement.

    int first_zero_row = -1     /* none */
    int i, j;
    for (i=0; i<n; i++) {
      for (j=0; j<n; j++) {
        if (A[i][j]) goto next;
      }
      first_zero_row = i;
      break;
    next:;
    }
  
The intent of the code is to find the first all-zero row, if any, of an n×n matrix. Do you find the example convincing? Is there a good structured alternative in C? In any language?

Multilevel Returns

Errors and Other Exceptions

  begin                              begin
    x := y/z;                          x := y/z;
    z := F(y,z); -- function call      z := F(y,z);
    z := G(y,z); -- array ref          z := G(y,z);
  end;                               exception   -- handlers
                                       when Constraint_Error =>
                                         -- do something
                                     end;

The Ada code on the right illustrates exception handling. An Ada constraint error signifies using an incorrect value, e.g., dividing by zero, accessing an array out of bounds, assigning a negative value to a variable declared positive, etc.

Ada is statically scoped but constraints have a dynamic flavor. If G raises a constraint error and does not have its own handler, then the constraint error is propagated back to the caller of G, in our case the anonymous block on the right. Note that G is not lexically inside the block so the constraint error is propagating via the call chain not scoping.

    (define f
      (lambda (c)
        (c)))
    (define g
      (lambda ()
        ;; beginning of g
        (call-with-current-continuation f)
        ;; more of g
        ))
  

6.2.2 Continuations

A continuation encapsulates the current context, including the current location counter and bindings. Scheme contains a function

      (call-with-current-continuation f)
    
that invokes f, passing to it the current continuation c. The simplest case occurs if a function g executes the invocation above and then f immediately calls c. The result is the same as if f returned to g. This example is shown on the right.

However, the continuation is a object that can be further passed on and can be saved and reused. Hence one can get back to a given context multiple times. Moreover, if f calls h giving it c and h calls c, we are back in g without having passed through f.

One more subtlety: The binding that contained in c is writable. That is, the binding is from name to memory-location-storing-the name, not just from name to current-value. So in the example above, if f changes the value of a name in the binding, and then calls c, g will see the change.

Continuations are actually quite powerful and can be used to implement quite a number of constructs, but the power comes with a warning: It is easy to (mis-)use continuations to create extremely obscure programs.

6.3: Sequencing

    for I in 1..N loop
      -- loop body
    end loop;
  
Many languages have a means of grouping several statements together to form a compound statement. Pascal used begin-end pairs to form compound statements, C shortened it to {}. Ada doesn't need {} because the language syntax already contains bracketing constructs. As an example, the code on the right is the Ada version of a simple C for loop. Note that the end is part of the for statement and not a separate begin-end compound.

When declarations are combined with a sequence the result is a block. A block is written

    { declarations / statements }   declare declarations begin statements end
  
in C and Ada respectively. As usual C is more compact, Ada more readable.

Another alternative is make indentation significant. I often use this is pseudo-code; B2/ABC/Python and Haskell use it for real.

  if condition then statements
  if (condition) statement

  if condition then
    statements_1
  else
    statements_2
  end if

  if condition_1 {
    statements_1 }
  else if condition_2 {
    statements_2}
  else {
    statements_3}

  if condition_1 then
    statements_1
  elsif condition_2 then
    statements_2
  ...
  elsif condition_n then
    statements_n
  end if

6.4: Selection

Essentially every language has an if statement to use for selection. The simplest case is shown in the first two lines on the right. Something is needed to separate the condition from the statement. In Pascal and Ada it is the then keyword; in C/C++/Java it is the parentheses surrounding the condition.

The next step up is if-then-else, introduced in Algol 60. We have already seen the infamous dangling else problem that can occur if statements_1 is an if. If the language uses end markers like end if, the problem disappears; Ada uses this approach, which is shown on the right. Another solution, used by Algol 60, is to forbid a then to be followed immediately by an if (you can follow then by a begin block starting with if).

When there are more than 2 possibilities, it is common to employ else if. The C/C++/Java syntax is shown on the right. This same idea could be used in languages featuring end if, but the result would be a bunch of end if statements at the end. To avoid requiring this somewhat ugly code, languages like Ada include an elsif which continues the existing if rather than starting a new one. Thus only one end if is required as we see in the bottom right example.

6.4.1: Short-Circuit Conditions (and Evaluation)

Can if (y==0 || 1/y < 100) ever divide by zero?
Not in C which requires short-circuit evaluation.

In addition to divide by zero cases, short-circuit evaluation is helpful when searching a null-terminated list in C. In that situation the idiom is

    while (ptr && ptr->val != value)
  
The short circuit evaluation of && guarantees that we will never execute null->val.

But sometimes we do not want to short-circuit the evaluation. For example, consider f(x)&&g(x) where g(x) has a desirable side effect that we want to employ even when f(x) is false. In C this cannot be done directly.

  if C1 and C2  -- always eval C2
  if C1 and then C2 -- not always

Ada has both possibilities. The operator or always evaluates both arguments; whereas, the operator or else does a short circuit evaluation.

Homework: 12. Neither Algol 60 nore Algol 68 employs short-circuit evaluation for Boolean expressions. In both languages, however, an if...then...else construct can be used as an expression. Show how to use if...then...else to achieve the effect of short-circuit evaluations.

  case nextChar is
    when 'I'    => Value := 1;
    when 'V'    => Value := 5;
    when 'X'    => Value := 10;
    when 'C'    => Value := 100;
    when 'D'    => Value := 500;
    when 'M'    => Value := 1000;
    when others => raise InvalidRomanNumeral;
  end case;

6.4.2 Case/Switch Statements

Most modern languages have a case or switch statement where the selection is based on the value of a single expression. The code on the right was written in Ada by a student of ancient Rome. The raise instruction causes an exception to be raised and thus control is transferred to the appropriate handler.

A case statement can always be simulated by a sequence of if statements, but the case statement is clearer.

(Alternative) Implementations

In addition to increased clarity over multiple if statements, the case statement can, in many situations be compiled into better code. Most languages having this construct require that the tested for values ('I', 'V', 'X', 'C', 'D', and 'M' above) must be computable at compile time. For example in Ada, they must be manifest constants, or ranges such as 5..8 composed of manifest constants, or a disjunction of these (e.g., 6 | 9..20 | 8).

In the simplest case where each choice is a constants, a jump table can be constructed and just two jumps are executed, independent of the number of cases. The size of the jump table is the range of choices so would be impractical if the case expression was an arbitrary integer. If the when arms formed consecutive integers, the implementation would jump to others if out of range (using comparison operators) and then use a jump table for the when arms.

Hashing techniques are also used (see 3e). In the general case the code produced is a bunch of tests and tables.

Syntax and Label Semantics

The languages differ in details. The words case and when in Ada become switch and case in C. Ada requires that all possible values of the case expression be accounted for (the others construct makes this easy). C and Fortran 90 simply do nothing when the expression does not match any arm even though C does support a default arm.

  switch (x+3) {
    case 2:  statements_2
             break;
    case 1:  statements_1
             break;
    case 6:  statements_6   // NO break between
    case 5:  statements_5   // cases 6 and 5
             break;
    default: something
  }

The C switch Statement

The C switch statement is rather peculiar. There are no ranges allowed and the cases flow from one to the other unless there is a break, a common cause of errors for beginners. So if x is 3, x+3 is 6 and both statements_6 and statements_5 are executed.

Indeed, the cases are really just statement labels. This can lead to quite unstructured code including the infamous ...

Duff's Device (R Rated—Don't Show This to Minors)
  void send (int *to, int *from, int count) {
    int n = (count + 7) /8;
    switch (count % 8) {
      case 0: do { *to++ = *from++;
      case 7:      *to++ = *from++;
      case 6:      *to++ = *from++;
      case 5:      *to++ = *from++;
      case 4;      *to++ = *from++;
      case 3:      *to++ = *from++;
      case 2:      *to++ = *from++;
      case 1:      *to++ = *from++;
                 } while (--n > 0);
    }
  }

This function is actually perfectly legal C code. It was discovered by Tom Duff, a very strong graphics programmer, formerly at Bell Labs now at Pixar. The normal way to copy n integers is to loop n times. But that requires n tests and n jumps.

Duff unrolled the loop 8 times meaning that he looped n/8 times with each body copying 8 integers. Unrolling is a well known technique, nothing new yet. But, if n is say 804, then after doing 100 iterations with 8 copies, you need to do one extra iteration to copy the last 4. The standard technique is to write two loops, one does n/8 iterations with 8 copies, the other does n%8 iterations with one copy. Duff's device uses only one loop.

Compiler technology has improved since Duff created the device and Wikipedia says that when it was removed from part of the X-window system, performance increased.

6.5: Iteration

  for I in A..N loop statements end loop;
  for (int i=A; i<n; i++) { statements }

6.5.1: Enumeration-Controlled Loops

On the right we see the basic for loop from Ada and C.

Code Generation For for Loops

Possibilities / Design Issues (Semantic Complications)

There is a tension between flexibility and clarity. An Ada advocate would say that for loops are just for iterating over a domain; there are other (e.g., while) loops that should be used for other cases. A C advocate finds the for(;;) construct an expressive and concise way to implement important idioms.

This tension leads to different answers for several design questions.

  for (expr1; expr2; expr3) statements;

  expr1;
  while (expr2); {
    statements;
  }
  expr3;

6.5.2: Combination Loops

The C for loop is actually quite close to the while loops we discuss below since the three clause within the for can be arbitrary expressions, including assignments and other side-effects. Indeed the for loop on the right is defined by C to have the same semantics as the while loop below it unless the statements in the body include a continue statement.

Loops combining both enumeration and logical control are also found in Algol 60 and Common Lisp.

6.5.3: Iterators

We saw above an example of a loop with range an enum Weekday=(Mon,Tues,Wed,Thurs,Fri). More generally, some languages, e.g., Clu, Python, Ruby, C++, Euclid, and C# permit the programmer to provide an iterator with any container (an object of a discrete type).

    for (thing=first(); anyMore(); thing=next()); {
      statements
    };
  

The programmer then writes first, next, and anyMore operators that in succession yield all elements of the container. So the generalization of the C for loop becomes something like the code on the right.

As an example imagine a binary tree considered as a container of nodes. One could write three different iterators: preorder, postorder, and inorder. Then, using the appropriate iterator in the loop shown above would perform a preorder/postorder/inorder traversal of the tree.

True Iterators

Iterator Objects

Iterating with First-Class Functions

Iterating without Iterators

Generators in Icon

6.5.5: Logically Controlled Loops

Essentially all languages have while loops of the general form

    while condition loop statements end loop;
  
In while loops the condition is tested each iteration before the statements are executed. In particular, if the condition is initially false, no iterations are executed.

In fact all loops can be expressed using this form, for suitable conditions and statements. This universality proves useful when performing invariant/assertion reasoning and proving programs consistent with their specifications.

  repeat statements until condition;
  do { statements } while (condition);

  first := true;
  while (first or else condition) loop
    statements
    first := false;
  end loop;

Post-test Loops

Sometimes we want to perform the test at the bottom of the loop (and thus execute the body at least once). The first two lines on the right show how this is done in Pascal and C respectively. The difference between the two is that the repeat-until construct terminates when the condition is true; whereas, the do-while terminates when the condition is false.

The do-while is more popular. One language that has neither is Ada so code like the bottom right is needed to simulate do-while.

  Outer: while C1 loop
    statements_1a
    Mid: while C2 loop
      statements_2a
      exit outer when condition_fini;
      inner:while C3 loop
        statements_3
        exit when condition_2b;
        exit mid when condition_1b;
      end loop inner;
      statements_2b
    end loop mid;
    statements_1b
  end loop outer;

Midtest Loops

More generally, we might want to break out of a loop somewhere in the middle, that is, at an arbitrary point in the body. Many languages offer this ability, in C/C++ it is the break statement.

Even more generally, we might want to break out of a nested loop. Indeed, if we are nested three deep, we might want to end the two innermost loops and continue with the outer loop at the point where the middle loop ends.

In C/C++ a goto is needed to break out of several layers; Java extends the C/C++ break with labels to support multi-level breaks; and Ada uses exit to break out and a labeled exit (as shown on the right) for multi-level breaks.

For example, if condition_fini evaluates true, the entire loop nest is exited; if condition_2b evaluates true, statements_2b is executed; and if condition_1b evaluates true, statements_1b is executed.

6.6: Recursion

We will discuss recursion again when we study subprograms. Here we just want to illustrate its relationship to iteration.

6.6.1: Iteration and Recursion

Although iteration is indicated by a keyword statement such as while, for, or loop, and recursion is indicated by a procedure/function invocation, the concepts are more closely related than the divergent syntax suggests. Both iteration and recursion are used to enable a section to be executed repeatedly.

  #include <stdio.h>
  int summation (int (*f)(int x), int low, int high) {
      int total = 0;
      int i;
      for (i = low; i <= high; i++) {
          total += (*f)(i);
      }
      return total;
  }
  int square(int x) {
      return x * x;
  }
  int main (int argc, char *argv[]) {
      printf ("Ans is %d", summation(&square,1,3));
      return 0;
  }

  (define square (lambda (x) (* x x)))
  (define summation
    (lambda (f lo hi)
      (cond
       ((> lo hi) 0)
       ((= lo hi) (f lo))
     (else (+ (summation f lo (- hi 1)) (f hi))))))
  (summation square 1 3)

  (summation (lambda (x) (* x x)) 1 3)

On the right we see three versions of a function to compute

    Sum (from i = lo) (to i = hi) f(i)
  
given three parameters, lo, hi, and (a pointer to) f.
  1. The first version uses iteration. It is written in C, which does not support passing functions as parameters, but does support passing pointers to functions. This explains the specification of the first parameter of summation. As an aside, C is willing to do some automatic type conversions (called coercions) so the & could be omitted and the loop body could say f(i). However, it is important to note that the first argument to summation is a pointer not a function.

  2. The second version is in scheme and uses recursion. The reason the recursion goes down from hi rather than up from lo is so that the arithmetic is done in the same order as with the iterative version.

  3. The third version uses the same definition of summation as does the second, but uses the anonymous function directly and thus doesn't need (or have) any mention of the name square.

Tail Recursion

A recursive function is called tail recursive if the last thing it does before returning is to invoke itself recursively.

In this case we do not need to keep multiple copies of the local variables since, when one invocation calls the next, the first is finished with its copy of the variables and the second one can reuse them rather than pushing another set of local variables on the stack. This is very helpful for for performance.

  int gcc (int a, int b) {               int gcc (int a, int b) {
    				         start:
    if (a == b) return a;      	           if (a == b) return a;
    if (a > b) return gcd(a-b,b);          if (a > b) {
                                             a = a-b; goto start; }
    return gcd(a,b-a); }                   b = b-a; goto start; }

The two examples on the right are from the book and show equivalent C programs for gcd: the left (tail-) recursive, the right iterative. Both versions assume the parameters are positive. Good compilers (especially, those for functional languages will automatically convert tail recursive functions into equivalent iterative form.

Thinking Recursively

I very much recommend the book The Little Schemer by Friedman and Felleisen.

6.6.2: Applicative and Normal-Order Evaluation

Lazy Evaluation

6.7: Nondeterminacy