Suggested Problems
We have provided below a list of suggested problems that you can use to prepare for the final exam. We strongly encourage you to solve these problems independently. We will release solutions to selected problems after the "open office hours" on Monday, December 13.
If you have any questions about the problems, feel free to post your questions/thoughts on the newsgroup. As opposed to regular homework problems, the solutions to these problems will not be graded. We thus encourage you to post and discuss solutions openly.
Check this page every now and then for updates.
The first group of problems that we provide refers to the Mini-ML interpreter. To provide more context we have embedded these problems into the lecture notes for lecture 25. Please follow the link we have provided for each problem; the text of the problem itself might not always be intelligible without the context that the lecture notes provide.
Feel free to use the Mini-ML handouts that you were given in class when solving the problems below. We will provide you with handouts during the exam as well.
- P1-3: (P1) Define values for empty_env and empty_typenv so that a recursive function is predefined as well (say, factorial). (P2) Can you predefine two mutually recursive functions? (P3) If yes, can you extend your idea to predefine groups of more than two mutually recursive functions? Note: remember that the user can not define such groups of mutually recursive functions from the command prompt.
- P4: Is it correct - or necessary - to treat the tuple types so that they can degenerate to a (non-tuple) exception type? Does it make any difference? Explain your answer. While developing your answer, consider alternative implementations like the one below:
| (TUPLE_T lt1, TUPLE_T lt2) =>
if not(length lt1 = length lt2)
then raise StaticTypeError err_str
else let
TUPLE_T (map (fn (t1, t2) => type_unification(t1, t2, err_str)
(ListPair.zip(lt1, lt2)))
end
- P5: By examining the implementation of type checking on anonymous functions (FN_E), explain whether the mini-ML code below passes type checking or not. If type checking is passed, infer the result that mini-ML prints after evaluation. If a type error is detected, explain why it is so.
((fn (n: int) => raise Fail "bad") 3) ^ "alpha"
- P6-7: (P6) We could allow the shadowing of special forms. In other words, if a user-defined function with a name identical to that of a special form is available in the current environment, then we could use the function, and not the special form. (P7) Alternatively, we could prevent functions with names identical to special forms from being defined.
Perform the changes changes needed to modify the interpreter to implement the two alternative semantics described above.
- P8: Returning to the treatment of APP_E, we also note the treatment of exceptions. If, while processing an expression of the form e1 e2 we encounter an exception while evaluating e1, then the evaluation of e2 will not even begin. We could have decided to temporarily ignore the exception that results from e1 and evaluate e2 anyway (of course, ultimately we would still have to return an exception).
Modify the interpreter so that you implement the treatment of mini-ML exceptions in function applications as described above. Comment on the impacts of this change in semantics in terms of execution time, resource utilization, and other factors that you find relevant.
- P9-10: Consider the following piece of mini-ML code:
let
val x = 3
in
let
fun f1(n: int): int->int = if x = 0 then (raise Fail "failed")
else (fn (y:int) => y + n)
in
let
fun f2(n: int): int = if x = 0 then f2(n) else x
in
(f1(3)) (f2(5))
end
end
end
Keeping in mind that all mini-ML programs must be written on one line, and that the code above has been edited for readability, state what the code will evaluate to for various values of x (P9) assuming the default treatment of exceptions in mini-ML, and (P10) assuming that the treatment of exceptions has been changed as described above.
- P11: Consider special form if_maybe, for which we have provided a stub in the interpreter. Special form if_maybe takes exactly two arguments; when called, it randomly evaluates either the first or the second one, and returns the value of the evaluated expression. Error situations must be treated by raising appropriate mini-ML exceptions.
- P12: Special form handle_eval takes exactly two arguments. The first argument is evaluated unconditionally; if any exception results from the evaluation of the first expression, then the second argument is evaluated and its value is returned as the value of the special form. This form behaves similarly to the handle statement, with the difference that the special form will catch all exceptions, irrespective of their type.
- P13: Special form evaluate_default takes exactly two arguments. The first expression must evaluate to a non-exception value that will be used as the default value for all variables that are free in the second argument, and which are not defined in the current environment. Consider the following example:
Mini-ML> let val x = 3 in evaluate_default(false, if y then x else x + 5) end
In the case illustrated above the mini-ML should evaluate to 8.
- P14-17: Implement type checking for (P14) if_specialform, (P15) if_maybe, (P16) handle_eval, and (P17) evaluate_default.
The order of the problem is not significant, it merely reflects the order in which problems have been added to this list. Problems earlier in the list are not necessarily more/less important, or easier/harder than problems toward the end of the list.
- P18: Consider an initially empty binary search tree, and the following sequence of operations:
(a) Insert numbers 5, 4, 6, 3, 7, 2, 8, 1, 9, in this order, using the regular insertion procedure for binary search trees. Draw the resulting tree.
(b) Treating the tree as a splay tree, look up node 1. Draw the resulting tree.
(c) Continuing to treat the tree as a splay tree, look up node 9. Draw the resulting tree.
- P19: Write an SML program that creates the simplest datastructure you can find such that that a simple mark-and-sweep garbage collector can not recover the space allocated to the respective data structure. Explain what you did. Show the environment diagram that corresponds to your solution.
- P20: Consider the following SML code skeleton:
let
(* 1 *)
fun f1 ... = ... f2 ... f3 ...
(* 2 *)
and f2 ... = ... f3 ... f1 ...
(* 3 *)
and f3 ... = ... f1 ... f2 ...
(* 4 *)
in
...
end
We have skematically represented three mutually recursive functions. Show the environment diagram at points (1), (2), (3), and (4) above.
- P21: Explain the difference between the let ... in ... end statement and the local ... in ... end statement.
- P22: Implement function foldr without consulting your notes. State whether your solution is tail recursive or not. Implement foldr using the "other" type of recursion. You must provide purely functional implementations in both cases. Should you need to, you can use a helper function for either - but not both - implementations of foldr.
- P23: Provide two short SML expressions that show that applying foldl and foldr to the same list and using the same initial value for the accumulator can yield (a) different results, and (b) the same result in both cases.
- P24: Ordinarily, we can garbage collect the memory region allocated for bindings in a let statement as soon as the evaluation of the respective statement ends. Consider the example below:
let
val x: int = 7
val y: int = 9
in
x + y
end
Provide an example in which at least one binding created in a let statement must be preserved (i.e. it can not be removed, or garbage collected) as soon as the evaluation of the respective let statement ends.
- P25: Provide a tail-recursive implementation for the following function (a, b are real numbers)
H(m,n) = a * H(m-1,n) + b*H(m-1,n-1)
H(0,0)=1.0
H(m,n)=0 if m
Note: This problem is hard; it was posted as a challenge on the newsgroup earlier this semester; Tibor provided a solution that you can look up there.
- P26: State the type of the expressions below
(a) ((((()))))
(b) (((((1)))))
- P27: Consider the following type definition:
datatype values = Null | INT of int | REAL of real
What is the value of the expression below:
let
fun f(v: values) =
case v of
NULL => 0
| INT i => i * i
| REAL r => Real.floor (r * r)
in
REAL 2.5
end
Hint: This is a trick question.
- P28: Consider the usual definition of streams below
datatype 'a stream = Null | Stream of 'a * (unit -> 'a stream)
Write function concat which takes a stream of integer streams, and returns a stream of integers which is the concatenation of the streams from the function's arguments. For example,
val s1 = Stream(1, fn () => Null)
val s2 = (Null: int stream)
val s3 = Stream(2, fn () => Stream(3, fn () => Null))
concat(Stream(s1, fn () => Stream(s2, fn () => Stream(s3, fn () => Null))))
should return Stream(1, fn() => Stream(2, fn () => Stream(3, fn () => Null))). Keep in mind that streams can be infinite.
- P29: Write a mini-ML program that can be used to establish whether the interpreter uses static or dynamic scoping. State the values that your program would return in both cases. Draw the environment diagram both for the static and the dynamic scoping case and use these diagrams to explain the reasoning behind your solution.
- P30: REPLACE strings ford, type, and marvin so that the expression below evalutes to (2004, 312):
let
fun ford: type = marvin
in
(39 * 8, 167 * 12)
end