[UMN logo]

CSCI 5106: Programming Languages
Spring 2006, University of Minnesota
Homework 7


Posted: April 11, 2006
Due: Before class on April 25, 2006


Note: All the programming parts of this assignment are to be done in ML. There are also certain parts in this question that ask for the types of expressions. You may want to check your answers to these parts using the ML system.


Problem 1

In Problem 3 of Homework 5, you defined a type for representing lists whose elements were of arbitrary, but homogenous, type and a function for mapping a function over the elements of such a list. Both these are built into ML. If they weren't they could have been defined as follows:
    datatype 'a list = nil 
                     | :: of 'a * 'a list
    infixr  5  ::

    fun mymap f nil = nil
      | mymap f (x :: l) = (f x) :: mymap f l
Your task in this problem is to contrast this definition qualitatively with the one in C.
  1. In an implementation of ML, a list would, in the first approximation, be represented as a pointer to a tagged memory cell. If the tag on this cell indicates it to be a cons (i.e. ::), then the next two cells would contain pointers to the head and the tail. Compare this kind of representation to the one you should have provided in C; look at the comments page for Homework 5 for a reference type declaration.

  2. Present the type that is inferred for mymap if it is defined as shown above and explain the process by which this type is inferred.

  3. Explain the significance of the type inferred for mymap. In particular, explain how the type allows mymap to be used to map functions over different types of lists and contrast also the type checking capabilities you have here with those you had with the mapping function you defined in Problem 3 of Homework 5.


Problem 2

In propositional logic, logical expressions are built starting from propositional variables by using logical operators. For the purposes of this problem, we will assume that these operators are and, or and not. Thus, if p and q are propositional variables, then and(or(p,q),not(p)) is a logical expression. Expressions of this kind are given a truth value based on an assignment of truth values to the propositional variables that appear in them and our usual understanding of the logical operators. Thus, if p is assigned the value false and q is assigned the value true, then the expression and(or(p,q),not(p)) is deemed to be true.
  1. Provide a datatype declaration in ML that can be used to represent logical expressions as described above. You may assume that propositional variables are represented by strings.

  2. Explain how you might represent an assignment of truth values for a collection of propositional variables.

  3. Define a function eval that takes a logical expression E and an assignment L of truth values for propositional variables, both represented in the ways you have just described, and that returns the truth value of E under the assignment L.


Problem 3

This question relates to dealing with infinite objects and requires you to have read the handout on lazy and eager evaluation first. There are actually two parts to the question of which you are required to answer only one, noting that the first part carries 10% extra credit.

Part 1.

Explain how the ML code for finding primes that appears at the end of the handout works. In particular, you should explain the underlying algorithm that is used as well as the manner in which finite encodings of infinite objects are realized and used in defining prime that actually represents the infinite sequence of primes. Note that brevity, clarity and insightfulness are important characteristics of any explanation and your answer will be judged for these properties in addition to correctness.

Part 2.

Here we deal with the simpler task of explaining how infinite objects may be represented and used in ML.
  1. This is not difficult to do in a functional programming language with a lazy evaluation rule. For instance, the following defines the stream of natural numbers in such a situation
       local
          fun natnumfn n = n :: natnumfn (n + 1)
       in val natnums = natnumfn 0
       end
    
    However, if you were to try to define the stream in this fashion in ML you run into problems. Explain what these problems are.

  2. The standard way to delay the evaluation of expressions in an eager language is to convert them into a function of a dummy argument. As a particular example, the notion of a stream can be defined through the following datatype and associated accessor and constructor functions in ML:
      datatype 'a stream = stream of (unit -> ('a * 'a stream) )
    
      fun next(stream f) = f()
      val mkstream = stream
    
    This type can be used to define the stream of numbers starting from a given one and a function for extracting the first few elements of a possibly infinite stream as follows:
      fun nat n = mkstream (fn () => (n, nat (n + 1)))
    
      fun firstm 0 fx = []
        | firstm m fx = let val (fst,rst) = next fx
                        in  fst :: firstm (m-1) rst
                        end
    
    Explain the structure of the stream datatype and how exactly it is being used in the code above to perform meaningful computations on infinite objects. You may use an example calculation, such as (firstm 5 (nat 0)).


Problem 4

In this problem we are going to simulate the control flow constructs in an imperative programming language within ML. In doing this successfully, we have to deal with the notion of state that is so central to imperative programming languages. Indeed, the main constructs in such languages are ones for using and modifying state and the result of computation is the state that is eventually produced. This is, of course, an abstraction. Typically we are interested only in snapshots of states, indicated by print statements or returned as results by functions/procedures. However, we can live with this abstraction for the moment. Given this, we can simulate imperative programs in a functional programming context as follows:
  1. First, we represent states as tuples of values. For example, if there are only two program variables x and y whose values are 1 and 2 respectively, then we could represent this state by the tuple (1,2).

  2. Next, we think of expressions as functions from tuples (representing states) to values. To continue with the above example, suppose that the (imperative) program we are trying to encode uses only the variables x and y and that we encode states involving these as described. Then the expression x + y may be encoded as the function
           (fn (x,y) => x + y)
    

  3. Since we view states as tuples of values, statements become tuple transformers for us, i.e. they are functions that take tuples as input and return tuples as results. Continuing with the earlier example, assignment to the variable x can be simulated by the function
       fun assignx exp state = let val (x,y) = state
                               in ((exp state),y)
                               end
    
    and a statement of the form x := E translates to the expression (assignx E'), where E' represents the translation of E. To be even more concrete, x := x + y becomes the expression (assignx (fn (x,y) => x + y)).

  4. Finally, we need to simulate the control flow constructs. These take statements and possibly expressions and produce new statements. Thus, we can realize these through higher order functions. For example, sequencing can be defined, exactly as you would imagine, as the composition of two statements
       fun seq stmt1 stmt2 = (fn x => (stmt2 (stmt1 x)))
    
    and the if-then-else statement can be realized via the function ifstat defined as follows:
       fun ifstat test stmt1 stmt2 = 
                (fn x => if (test x) then (stmt1 x) else (stmt2 x))
    
Your task in this problem is twofold. First, write definitions of the functions whilestat and repeatstat that simulate the while-do and the repeat-until statements from our language for structured programming. Then write out at least two simple imperative programs using only the collection of constructs described (programs such as the ones for gcd, exponentiation, factorial and fibonnaci may be good examples), translate them into the corresponding ML expressions and test your translations and the implementations of while-do and repeat-until by evaluating these expressions.

Note: A natural tendency is to look for imperative constructs in ML matching the while-do and the repeat-until in imperative programming languages. This is the wrong thing to do for this problem. We are trying here to simulate imperative programming in a functional programming setting and not to mimic imperative behaviour.


Problem 5

Problem 9.10 in the book.


Problem 6

Suppose we want sum up the elements in an integer list. We could write a simple recursive function to do this as follows:
   fun sumup nil = 0
     | sumup (a::y) = a + (sumup y)
While this function works, it is not very efficient. The problem is that when the recursive call is finished, you have to return to the calling procedure to carry out an addition before returning the value. As a consequence, the state in the calling procedure has to be preserved, leading to a usage of space proportional to the length of the list. This is quite different from the situation in an iterative program (i.e. a one with a while loop) that you might write to add up the list elements.

Fortunately, it turns out that by being careful you can structure your recursion so that it is just like the iteration. Thus, consider the following alternative definition of sumup:

  fun sumup [] n = n
    | sumup (a::y) n = sumup y (a + n)
Given a list L, the desired result is obtained by evaluating the expression (sumup L 0). The second argument of this function acts like an `accumulator' that carries along a partially computed sum. The noteworthy aspect of the new sumup is that when it makes a recursive call, this call completely determines the value to be returned, i.e. instead of returning to the calling point one could directly return to the point from where sumup was called. Thus, there is no need to save the activation space for the caller at the time of carrying out the recursive call. Functions that have this character are said to be iterative or tail recursive. Notice that, just like the case for iteration, such functions can be executed using constant space, or, alternatively, reusing the same stack space for each call. Compilers for functional programming languages are typically engineered to exploit this possibility---for Scheme this is even a requirement that is included in the language manual. Thus you can get the elegance of recursion for the cost of iteration.

This problem requires you to try your hand at defining a function that is iterative like sumup but that works over integer binary trees. In particular, let such trees be given by the following datatype declaration:

    datatype inttree = Empty
                     | Node of int * inttree * inttree
Your function---that we will refer to as sumtree---must work over this datatype and must be tail-recursive. Just like sumup, it may need to take an extra accumulator argument and it may also need to receive the tree packaged inside another structure like a list.

Hint: Think of generalizing sumtree to work over lists of trees or forests. Doing it this way will allow you to pass along information about all the remaining work to the recursive call.


Last updated on April 11, 2006 by gopalan@cs.umn.edu