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.
- 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.
- 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.
- 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.
- 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.
- Explain how you might represent an assignment of truth values for
a collection of propositional variables.
- 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.
- 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.
- 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:
- 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).
- 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)
- 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)).
- 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