Assignment 3: Collections API
Due: Wednesday, October 17 at 4:00pm
Submission cutoff: Saturday, October 20 at 4:00pm
In this assignment, you will implement modules for a few polymorphic data structures in OCaml and the typed lambda calculus. You will gain experience with advanced OCaml features (higher order functions, user defined types, recursive data structures) and understand their theoretical analogs.
1: List (40%)
Recall from class the venerable linked list, represented in OCaml by the recursive type Nil | Cons of 'a * 'a list. Your task is to implement the functions on the List2 type signature:
module type List2 = sig
type 'a list = Nil | Cons of 'a * 'a list
val foldr : ('a -> 'b -> 'b) -> 'b -> 'a list -> 'b
val map : ('a -> 'b) -> 'a list -> 'b list
val filter : ('a -> bool) -> 'a list -> 'a list
val reduce : ('a -> 'a -> 'a) -> 'a list -> 'a option
val combine_keys : ('a * 'b) list -> ('a * ('b list)) list
val to_string : ('a -> string) -> 'a list -> string
end
With the exception of foldr, every function must not be defined recursively, or with recursive helper functions. (Hint: define every function in terms of foldr.)
-
foldr : ('a -> 'b -> 'b) -> 'b -> 'a list -> 'bFoldr, or “fold right”, is a function that iterates over a list and repeatedly applies a function on each element, accumulating a piece of state along the way. Foldr defines the most generic way to iterate over the list data structure. For example:
let f = fun x acc -> x + acc in foldr f 0 (Cons(1, Cons(2, Cons(3, Nil)))) = f 1 (f 2 (f 3 0)) (* evaluates to 6 *) -
map : ('a -> 'b) -> 'a list -> 'b listMap applies the input function to each element of the list.
map (fun x -> x + 1) (Cons(1, Nil)) = Cons(2, Nil) -
filter : ('a -> bool) -> 'a list -> 'a listFilter takes a predicate, or function from a list element to a boolean, and returns a new list with only the elements for which the predicate is true.
filter (fun x -> x mod 2 = 0) (Cons(1, Cons(2, Nil))) = Cons(2, Nil) -
reduce : ('a -> 'a -> 'a) -> 'a list -> 'a optionReduce takes a function that combines adjacent elements, and reduces the list down to a single element. Reduce returns
Noneif the input list is empty.reduce (+) (Cons(1, Cons(2, Nil))) = Some(3) -
combine_keys : ('a * 'b) list -> ('a * ('b list)) listThis function takes a list of (key, value) pairs, matches up pairs with identical keys, and combines their values into a list.
combine_keys (Cons(("a", 1), Cons(("a", 2), Cons(("b", 1), Nil)))) = Cons(("a", Cons(1, Cons(2, Nil))), Cons(("b", Cons(1, Nil)), Nil)) -
to_string : ('a -> string) -> 'a list -> stringThis function takes a function that converts list elements to strings, and then turns the entire list into a string.
to_string string_of_int (Cons(1, Cons(2, Nil))) = "1 2 "
2: Stream (40%)
Your next task is to implement an infinite data type, that of streams. A stream is essentially an iterator that always returns a value.
module type Stream2 = sig
type 'a stream = Stream of (unit -> 'a * 'a stream)
val head : 'a stream -> 'a
val tail : 'a stream -> 'a stream
val take : 'a stream -> int -> 'a list * 'a stream
val zip : 'a stream -> 'b stream -> ('a * 'b) stream
val enumerate : 'a stream -> (int * 'a) stream
val windows : 'a stream -> int -> ('a list) stream
end
Read the type definition carefully. Because OCaml does not support recursive types outside of variants, we represent streams as a variant with one branch. The type of a stream is unit -> 'a * 'a stream, or a function that produces both a value and a resultant stream when called. For example, here’s a stream that always produces the same value:
let rec repeat (n : int) : int stream =
Stream (fun () -> (n, repeat (n)))
-
head : 'a stream -> 'aHead returns the current/first element in the stream.
let s = repeat 1 in (head s) = 1 -
tail : 'a stream -> 'a streamTail returns the stream starting at the next element.
let s = repeat 1 in let s = tail s in (head s) = 1 -
take : 'a stream -> int -> 'a list * 'a streamTake removes the first elements from a stream, returning those elements and the remaining stream.
let s = repeat 1 in let (l, s) = (take s 3) in l = Cons(1, Cons(1, Cons(1, Nil))) -
zip : 'a stream -> 'b stream -> ('a * 'b) streamZip takes two streams and produces a new stream of pairs of their items.
let s = zip (repeat 1) (repeat 2) in (head s) = (1, 2) -
enumerate : 'a stream -> (int * 'a) streamEnumerate takes a stream and returns a new stream where each item is indexed from 0 to infinity.
let s = enumerate (repeat 1) in (head s) = (0, 1) && (head (tail s)) = (1, 1) -
windows : 'a stream -> int -> ('a list) streamWindows takes a stream and produces a new stream of fixed size sliding windows over the input stream.
let s = windows (enumerate (repeat 1)) 2 in (head s) = Cons((0, 1), Cons((1, 1), Nil))
Additionally, you must implement the two stream test functions in StreamTests of upfrom and fib.
-
upfrom : int -> int streamThis function takes a number
n, and returns a stream that counts to infinity by 1 starting atn. -
fib : unit -> int streamThis function creates a stream of the Fibonacci numbers.
3: Stream calculus (20%)
Lastly, you’re going to translate a subset of your OCaml solutions to part 2 into the typed lambda calculus. Using the semantics for fixpoints, recursive types, and polymorphic types as presented this past week, implement the functions for tail, enumerate, and upfrom with the same semantics as above.
For example, the implementation of head is:
A few hints:
- Remember that the typed lambda calculus is more explicit than OCaml. Don’t forget your necessary folds/unfolds and type functions/type function applications.
- You will need to use the
fixoperator to emulatelet rec. - In our solution, the implementation logic did not change between OCaml and lambda calculus, just the type machinery around recursive/polymorphic types.
Submission
To submit your work, we have two Gradescope assignments for the written and programming sections. For the written section, upload your assign3.pdf file. For the programming section, upload your list2.ml and stream2.ml files.