CS 334
Programming Languages
Spring 2002

Assignment 5 Solutions
Due Friday (really!), 3/15/2002


  1. Please do problem 6.29a,b on pages 6-52 and 6-53.

    a. This function (or at least its curried variant) is sometimes known as the doubling function as it applies a function twice:

    - fun f (g,x) = g (g x);
    val f = fn : ('a -> 'a) * 'a -> 'a
    
    b. The type can be inferred as follows:

    Putting all of this together, we get

    'a -> 'b = ('d -> 'd) * 'd -> 'd
    
    as the final type of f.

  2. Please read about applicative and normal-order evaluation in Chapter 7 of Louden and do problem 7.14 on page 7-32.

    Given the two functions (in C syntax):

    int cube (int x) { return x*x*x; }
    int sum (int x, int y, int z) { return x + y + z; }
    
    describe the process of normal-order evaluation of the expression sum(cube(2), cube(3), cube(4)), and compare it to applicative order evaluation. In particular, how many additions and multiplications are performed using each method.

    Normal-order evaluation is essentially call-by-name. Thus arguments are evaluated only when they are needed. Thus the above expression would be evaluated as follows:

    sum(cube(2), cube(3), cube(4)) -> cube(2) + cube(3) + cube(4)
                                   -> 2 * 2 * 2 + 3 * 3 * 3 + 4 * 4 * 4
                                   -> 8 + 27 + 64
                                   -> 99
    							   

    With applicative order, we get the following evaluation order instead:

    sum(cube(2), cube(3), cube(4)) -> sum(2 * 2 * 2, 3 * 3 * 3, 4 * 4 * 4)
                                   -> sum(8, 27, 64)
                                   -> 8 + 27 + 64
                                   -> 99
    							   

    Alas, there is no difference in the number of additions or multiplications in the two different ways of evaluating this expression. Each takes 6 multiplications and 3 additions.

    If instead of sum we used:

    int triple (int x) { return x + x + x; }
    
    and then evaluated triple(cube(2)), then we would have seen 6 multiplications in one case and only 2 in the other.

  3. The "if ... then ... else ..." construct in ML satisfies the following rules for all expressions first and second: here expressions e1 and e2 are equivalent iff either both are undefined or both are defined and have the same value.

    Suppose we define the function cond in ML as follows:

       fun cond boolval first second = if boolval then first else second;
       

    a. Find expressions for first and second so that cond true first second is not equivalent to first.

    b. Find expressions for first and second so that cond false first second is not equivalent to second. Hint: The answers to (a) and (b) are similar -- and yes, it IS a trick question!

    c. Explain the different results in terms of eager (applicative) and lazy (normal order) evaluation.

    Part c of this question gives the hint that the difference between cond and if-then-else has to do with eager versus lazy evaluation. Thus key to the solutions to these three parts is the theorem stated half-way through lecture 5:

    General theorem: Let E be a functional expression (with no side effects). If E converges to a value under eager evaluation then E converges to the same value with lazy evaluation (but not vice-versa!!)

    ML uses eager evalution (applicative order) for evaluating function applications. Yet an if-then-else, if bcond then texp else fexp expression is not evaluated eagerly. First the boolean condition is evaluated. If it evaluates to true then texp is evaluated, otherwise fexp is evaluated. However, only one of the two is evaluated. Of course, this is absolutely critical or the evaluation of applications of recursive functions like

    fun fact n = if n = 0 then 1 else n * fact (n-1)
    
    would never terminate (the recursive call would always be evaluated, whether needed or not)!

    Now that we realize that the major difference between the cond function and if-then-else expressions is evaluation order, we can use the theorem to help find where such differences could arise.

    The theorem states that you always get the same answer with eager versus lazy evaluation unless one of two things happens: there are side effects or the eager evaluation does not converge to an answer. A computation can fail to converge to an answer if either it blows up or it never halts.

    For each of the three exceptions to the theorem, examples are possible showing that cond can give a different answer from if-then-else. Let's just examine part a, as part b is essentially identical. It doesn't really matter what we choose for expression first, as long as it converges to an answer. For concreteness, let first be 3 + 2. The trick here is to find an expression for second that will cause problems. Let's find examples for all three cases:

  4. The function string_to_num defined below uses two auxiliary functions to convert a string of digits into a non-negative integer.
       fun char_to_num c = ord c - ord #"0";
    
       fun calc_list ([],n) = n
         | calc_list ((fst::rest),n) = 
                           calc_list(rest,10 * n + char_to_num fst);
    
       fun string_to_num s = calc_list(explode s, 0);
    For instance, string_to_num "3405" returns the integer, 3405. Unfortunately, calc_list returns a spurious result if the string contains any non-digits. For instance, string_to_num "3a05" returns 7905, while string_to_num " 405" returns ~15595. This occurs because char_to_num will convert any character, not just digits. We can attempt to fix this by having char_to_num raise an exception if it is applied to a non-digit.

    a. Please revise the definition of char_to_num to raise this exception, and then modify the function string_to_num so that it handles the exception, returning ~1 if there is a non-digit in the string. You should make no changes to calc_list.

    exception badNum;
    
    fun char_to_num c = if c < #"0" orelse c > #"9" then raise badNum 
                               else ord c - ord #"0";
    
    fun calc_list ([],n) = n
      | calc_list ((fst::rest),n) = 
                           calc_list(rest,10 * n + char_to_num fst);
    
    fun string_to_num s = (calc_list(explode s, 0)) handle badNum => ~1;
    

    b. Please rewrite these ML functions to provide the same behavior (including returning ~1 if the string includes a non-digit) as in (a), but without using exceptions. While you may change any function, try to preserve as much of the structure of the original program as possible.

    fun char_to_num c = ord c - ord #"0";
    
    fun calc_list ([],n) = n
      | calc_list ((fst::rest),n) = let
               val numVal = char_to_num fst
            in if numVal < 0 orelse numVal > 9 then ~1
                           else calc_list(rest,10 * n + numVal)
            end;
    
    fun string_to_num s = calc_list(explode s, 0);
    

    This code is not significantly worse than the code with exceptions, but it does require a test in a strange part of the code, well away from where the error actually occurs (inside char_to_num).


Back to:
  • CS 334 home page
  • Kim Bruce's home page
  • CS Department home page
  • kim@cs.williams.edu