CSCI-GA 2110-003 - Final Exam Instructor: Thomas Wies Name: Sample Solution ID: You have 110 minutes time. There are 7 assignments and you can reach 110 points in total. You can solve the exercises directly on the exam sheet. If you need additional paper, ask the instructor. Do not forget to put your name and ID on every page that you hand in. assignment 1 2 3 4 5 6 7 Σ Grade max. points 25 10 13 18 12 12 20 110 - points Good Luck! 1
Assignment 1 (a) Write a polymorphic ML function Programming in ML (25 Points) zip: a list * b list -> ( a * b) list that takes a pair of lists and zips it to a list of pairs. The following examples demonstrate the intended behavior of zip: - zip ([1,2,3], [4,5,6]); val it = [(1,4),(2,5),(3,6)] : (int * int) list - zip ([1,2], [4,5,6]); val it = [(1,4),(2,5)] : (int * int) list - zip ([1,2,3], [] : int list); val it = [] : (int * int) list Note that if the input lists do not have the same length, then the remaining elements of the longer list are dropped. Your function does not need to be tail-recursive. (10 Points) (b) Write a polymorphic ML function foldr2: ( a * b * c -> c) -> c -> a list * b list -> c that is similar to the ML function foldr but folds two lists together. For example, the following function uses foldr2 to compute the sum of products of two lists of integers: fun sumofproducts (xs, ys) = foldr2 (fn (x, y, s) => x * y + s) 0 (xs, ys) In particular, sumofproducts ([1,2,3],[4,5,6]) should expand to 1 * 4 + (2 * 5 + (3 * 6 + 0)) and sumofproducts ([1,2,3],[4,5]) should expand to 1 * 4 + (2 * 5 + 0) Similar to the function zip, the function foldr2 should discard the remaining elements of the longer list, if the two input lists are not of the same length. (10 Points) (c) Express the function zip using the function foldr2. (5 Points) 2
Solution to Assignment 1: (a) fun zip ([], _) = [] zip (_, []) = [] zip (x::xs, y::ys) = (x,y)::zip (xs,ys) fun foldr2 f acc ([], _) = acc foldr2 f acc (_, []) = acc foldr2 f acc (x::xs, y::ys) = f (x, y, foldr2 f acc (xs, ys)) (b) (c) fun zip (xs, ys) = foldr2 (fn (x,y,xys) => (x,y)::xys) [] (xs, ys) 3
Assignment 2 Type Checking and Type Inference (10 Points) Consider the following ML function declaration fun blurgh (x, y) = if x - y then x * y else x / y handle Div => "Error: Division by 0" This declaration is syntactically correct. However, it contains two type errors. Find and explain these errors. 4
Solution to Assignment 2: First error. The term x-y has either type int or type real, depending on how the interpreter resolves the overloaded arithmetic operations. Either way, the term following after the if in the conditional expression must be of type bool. Second error. The handler for the Div exception returns a value of type string. However, the if expression that is guarded by the handler returns a value of type int or real (again depending on how overloaded operators are resolved). Either way, the types are incompatible. In ML, an exception handler must always return a value of a type that is compatible with the type of the expression that it guards. 5
Assignment 3 Type Inhabitation (13 Points) Write polymorphic ML functions that satisfy the following type signatures: where ˆ a function f: a -> b -> a ˆ a function g: ( a -> b -> c) -> b -> a -> c ˆ a function h: ( a, b) sum * ( a -> c) * ( b -> c) -> c ˆ a function i: ( a, b * c) sum -> ( b, a) sum * ( c, a) sum datatype ( a, b) sum = L of a R of b Do not use recursion or explicit type annotations in your declarations. Remember that in ML type expressions, the type constructor * has higher precedence than the type constructor ->. (3 + 3 + 3 + 4 Points) 6
Solution to Assignment 3: fun f a b = a fun g x b a = x a b fun h (L a, x, y) = x a h (R b, x, y) = y b fun i (L a) = (R a, R a) i (R (b, c)) = (L b, L c) 7
Assignment 4 Scala Traits and Dynamic Binding (18 Points) Consider the following Scala classes and traits: abstract class Term { def eval: Int trait Double extends Term { abstract override def eval = 2 * super.eval trait Increment extends Term { abstract override def eval = 1 + super.eval trait Nullify extends Term { abstract override def eval = 0 class Sum(x: Int, y: Int) extends Term { override def eval = x + y To which value does each of the following expressions evaluate (4 * 3 Points): (a) (new Sum(1,2)).eval (b) (new Sum(1,2) with Double with Increment).eval (c) (new Sum(1,2) with Increment with Double).eval (d) (new Sum(1,2) with Nullify with Increment).eval Explain your results. (6 Points) 8
Solution to Assignment 4: (a) 3. Here eval is bound to the eval method declared in the class Sum. (b) 7. Here eval is bound to the eval method declared in the trait Increment. This method calls the eval method of the class it is mixed in, which is the eval method of Double. This method, in turn, calls the eval method of Sum. The latter returns 3 which is rst multiplied by 2 and then incremented by 1. (c) 8. Here eval is bound to the eval method declared in the trait Double. This method calls the eval method of the class it is mixed in, which is the eval method of Increment. This method, in turn, calls the eval method of Sum. The latter returns 3 which is rst incremented by 1 and then multiplied by 2. (d) 1. Here eval is bound to the eval method declared in the trait Increment. This method calls the eval method of the class it is mixed in, which is the eval method of Nullify. The latter returns 0 immediately, which is then incremented by 1. 9
Assignment 5 Scala Generics (12 Points) Scala provides two traits Function1 and Function2 for objects representing unary and binary functions. They are dened as follows: trait Function1[-T1,+R] { abstract def apply(x: T1): R //... some other methods defined using apply trait Function2[-T1,-T2,+R] { abstract def apply(x: T1, y: T2): R //... some other methods defined using apply For example, we can dene an object Sum that extends Function2 as follows object Sum extends Function2[Int,Int,Int] { def apply(x: Int, y: Int) = x + y and then use it like this scala> Sum(3,4) res0: Int = 7 Write a trait CurriedFunction2 that is both a Function2 and a Function1 where the unary apply method of Function1 is the curried version of the binary apply method of Function2. For example, if we dene: object Sum extends CurriedFunction2[Int,Int,Int] { def apply(x: Int, y: Int) = x + y then we should be able to use the apply method of Sum also in its curried form: scala> Sum(3)(4) res1: Int = 7 Make sure that you provide the correct type parameters when you extend or mix in the traits Function2 and Function1. Also the variance annotations for the type parameters of CurriedFunction2 should be as general as possible. 10
Solution to Assignment 5: trait CurriedFunction2[-T1, -T2, +R] extends Function1[T1,Function1[T2,R]] with Function2[T1,T2,R] { def apply(x: T1) = (y: T2) => apply(x,y) The following solution also works: trait CurriedFunction2[-T1, -T2, +R] extends Function2[T1,T2,R] with Function1[T1,Function1[T2,R]] { def apply(x: T1) = (y: T2) => apply(x,y) 11
Assignment 6 Subtyping and Variances (12 Points) Consider the following two generic Scala classes: class CoVar[+T](x: T) { def method1: T = x def method2(y: T) : List[T] = List(x,y) class ContraVar[-T](x: T) { def method1: T = x def method2(y: T) : List[T] = List(x,y) The Scala compiler will reject both classes because at least one method in each class violates the variance annotation of the type parameter T of that class. (a) For each class, explain what these variance violations are. (6 Points) (b) Change each class such that the violations of variances are xed. You are not allowed to change any of the following: ˆ the variance annotation of the type parameter T; ˆ the type of the parameter x; ˆ the parameter names and bodies of the methods. However, you are allowed to introduce additional type parameters and type bounds, and change the argument and return types of methods. If you change any of the method types, they should remain as precise as possible (e.g., you are not allowed to use the types Any or Nothing). You might nd it more dicult to x the class ContraVar. Hint: introduce an additional type parameter for the whole class. (6 Points) 12
Solution to Assignment 6: (a) In the class CoVar the covariant type parameter T appears in contravariant position in the argument type of method method2. On the other hand, in the class ContraVar the contravariant type parameter T appears in covariant position in the result types of methods method1 and method2. (b) class CoVar[+T](x: T) { def method1: T = x def method2[u >: T](y: U) : List[U] = List(x,y) class ContraVar[+U, -T <: U](x: T) { def method1: U = x def method2(y: T) : List[U] = List(x,y) 13
Assignment 7 Exceptions and call/cc (20 Points) (a) Explain why it is useful to dene exceptions as classes in object oriented languages like C++, Java, and Scala. (4 Points) (b) Languages such as Java and Scala provide try-finally blocks for exception handling. What do they do and what are they used for? (4 Points) (c) How do exception handlers in functional languages like ML and Scala dier from those of imperative languages like C++ and Java? (4 Points) (d) Consider the following Scheme programs. evaluate to? (2 + 2 + 4 Points) Which value does each of these programs (i) (+ 4 (call/cc (lambda (c) 1))) (ii) (+ (+ 1 2) (call/cc (lambda (c) (+ (+ 1 2) (c 5))))) (iii) (define (search return wanted? lst) (if (wanted? (car lst)) (return (car lst)) (search return wanted? (cdr lst)))) (define (fluffy lst) (+ 1 (call/cc (lambda (return) (search return (lambda (x) (> x 3)) lst))))) (fluffy (1 2 3 4 5)) You do not need to explain your results to get full credit. Though, you might want to explain how you obtained the result for a chance of receiving partial credit in case your answer is incorrect. 14
Solution to Assignment 7: (a) This allows to group exceptions based on their subtype relationship. For example, one can dene a common exception handler for all exceptions that are subclasses of a specic class. (b) The finally block is executed when control leaves the try block, no matter whether this is due to normal termination of the block or because an exception was thrown but not caught in the try block. This is useful, for example, to ensure that all resources (such as locks, le descriptors, network sockets etc.) that have been acquired for usage in the current control block are properly released before control exits this block (e.g., because a thrown exception was not caught). (c) Unlike in imperative languages, where exception handlers are statements, in functional languages exception handlers return values just like any other expression. The result value of the exception handler is also the result value of the expression that is guarded by the handler in the case that an exception is thrown and caught by the handler. In statically typed languages such as ML and Scala, the type of the exception handler therefore has to be compatible with the type of the expression that it guards. (d) (i) 5 (ii) 8 (iii) 5. The function search calls the passed continuation return with the rst list element that satises the given predicate. This is the element 4. The continuation then increments this value by 1. 15