Homework 9: More Mutability
Due: Sunday, November 9th, 11:59pm
Start with the template code here.
- Replace each
failwith "TODO"with your solution. - Be sure to test your solutions in
utopor in tests alongside your submission. To load your file intoutop, use#use "hw9.ml";;. - Submit your version of
hw9.mlto the HW9 assignment on Gradescope. Only submithw9.ml; NOT any other files or folders. - At the top of the template file, there is a comment for you to write in approximately how many hours you spent on the assignment. Filling this in is optional, but helpful feedback for the instructors.
Q1: Combining Modules and Mutable State
Using mutable state, we can implement a notion of a "fresh integer" like so:
The intention is that the surrounding OCaml code can use the Fresh module and be assured that, whenever fresh : unit -> int is called, we truly get an integer that wasn't returned before by that function.
Unfortunately, the above module does not guarantee this property.
The following module does, however:
module type FreshSig = sig
val fresh : unit -> int
end
module FixedFresh : FreshSig = struct
let ctr = ref 0
let fresh () =
ctr := !ctr + 1;
!ctr
end
In a code example and accompanying comment, give a piece of code which shows that we can trick Fresh.fresh into returning the same integer on two different calls; and explain why the same code example won't work with FixedFresh.
The code example should return a pair of integers --- each coming from different calls to fresh --- but should return the same number in both components.
(* Return two integers --- both coming from different calls to Fresh.fresh --- that should always be equal. *)
let break_fresh () : int * int = failwith "TODO"
(* TODO: Why does the above code not work for FixedFresh?
---
*)
Q1: Grading Criteria
Whether break_fresh can return the same integer twice with two different calls to Fresh.fresh; and whether you comment explains why the same code won't work for FixedFresh.
Q2: Implementing References
As described in class, a value of type t ref is, in a sense, nothing more than a reference (or pointer) to a value of type t stored "somewhere else". When we call ref to create a new reference:
i, which indexes into a large buffer B of space. Then, when we call !x, we then index into the buffer using the index --- computing something like B[i] --- and grabbing the value there. If we call x := 5, then we are modifying this large buffer by setting B[i] := 5. The piece of code that manages this buffer B, and calculates and returns indices i, is called an allocator. If you have used C, then you have likely called malloc, which is this idea.
This is all happening under the hood, but we can bring this idea to light by building our own simple allocator in OCaml. The way we are going to do this is by having two types:
- An arena, which holds a large buffer of memory we can use (like our
Babove); and - An mref (standing for manual reference), which is an index into this arena.
The idea is that an arena is a large chunk of data we can use (and reuse) to
implement mutation. We actually use mutation via mrefs, which are just indices into the arena.
Our mref type willl support the normal operations of ref ---
creating new ones, assigning to them, and reading from them --- but also the
ability to free an mref, which allows another piece of code to reuse that
buffer space. When we free an mref, this is a promise that we will never read from that mref again; thus, further reads to the mref might fail.
We can implement these ideas using a module type:
module type ManualRefSig = sig
type 'a arena
type 'a mref
(* Create a new arena. The argument (capacity of the arena) must be nonnegative. *)
val fresh_arena : int -> 'a arena
(* Find an unused slot in the arena and store the given value there; and return an mref to that location.
Return None if no free space is available.
*)
val alloc : 'a arena -> 'a -> 'a mref option
(* Free the mref, which should allow that slot in the arena to be re-used.
If the mref points to an already freed location, the operation should do nothing. *)
val free : 'a arena -> 'a mref -> unit
(* Read an mref. Should return None if the mref is pointing to a freed location in the arena. *)
val read : 'a arena -> 'a mref -> 'a option
(* Write to an mref. Should return None if the mref is pointign to a freed location in the arena. *)
val write : 'a arena -> 'a mref -> 'a -> unit option
(* Do the two mrefs point to the same index in the arena? *)
val phys_eq : 'a mref -> 'a mref -> bool
(* Return the number of currently free cells in the arena. *)
val num_free : 'a arena -> int
(* Return the number of times we have called alloc on the arena. *)
val num_allocations : 'a arena -> int
end
Your task in this question is to implement ManualRefSig:
Importantly, the only actual allocation of mutable memory you should use shall be stored in the arena.
The 'a mref should merely store pure data (e.g., int), and should not hold a value of type 'a (since they are supposed to be stored in the arena).
To create a fresh arena, you should use (at least) OCaml's arrays, which
are similar to ref but holds N values at once, for a user defined N. You create arrays using Array.make, and you read/modify them using Array.get and Array.set, respectively.
While functions raise exceptions if you pass an out-of-bound index into the array, your code should never raise these exceptions.
(Note: you may also use ref inside of the arena in addition to an Array.t, if both are useful.)
Other than using Array.t inside arenas, and the fact that mrefs can't store mutable data (e.g., no ref, Array.t, or other mutable data structures from the standard library), the rest of the implementation is
up to you. Note that fresh_arena returns an 'a arena without specifying values for every slot; thus, the array inside the arena cannot hold 'a exactly, but a type that can optionally hold an 'a.
Additionally, note that since alloc must find an unused slot in the arena, the arena must keep track of what slots are used and which are free.
Finally, we also require that you implement functions num_free : 'a arena -> int and num_allocations : 'a arena -> int, to get the current number of free slots in the arena and the total number of allocations performed in the arena, respectively. (Thus, you need to keep track of this data as well in the arena.)
Q2: Grading Criteria
Correctness of your implementation of ManualRef.
Q3: Specifications about Memory
Now that we have our own model of memory, we can use it to reason about the behavior of code with respect to memory. In particular: have we freed every reference created? And how much memory are we using?
Ordinarily, answers to questions like these need to be made "outside" the program, using tools like Valgrind which instruments (adds additional checks to) your code to answer these questions.
However, by using num_free and num_allocations, we can answer these questions more directly.
To make the solutions to Q3 be generic over the particular implementation of ManualRefSig, we will use a functor that takes a particular implementation of the module as an argument:
open R inside of the functor so that we can make reference to type types 'a arena directly (instead of 'a R.arena).
You can test your code in this question by writing
at the bottom of the file, and calling the functions in / opening the moduleMyMemorySpecs.
Q3.1 First, we can implement a function leakage_free to ask if a certain function f : 'a arena -> unit has any memory leaks. A call to f has a memory leak whenever the number of free cells in the arena is different after f is called than before f is called.
f uses the arena a (i.e., no other arenas are used).
As an example of a memory leak,
leaks a single unit of memory, because we are storing 32 in the memory but throwing away the pointer --- thus, we can not free that memory later.
Q3.2 Next, we can implement a function num_allocations_in that computes the number of allocations done by the function:
Q3: Grading Criteria
Correctness of the two functions above (evaluated independently of the correctness of ManualRef from Q2).
Q4: Using Mutable Memory
In this question, we will explore an algorithm which uses mutable memory in a subtle way.
Our problem starts with computing the n'th Fibonacci number \(F(n)\), where we define \(F(0) = 0, F(1) = 1\), and \(F(n) = F(n - 2) + F(n - 1)\) for \(n \geq 2\).
We can easily implement this as a functional, recursive program:
n <= 0 instead of n = 0 just to make sure that fib never loops forever.)
The main problem with this function is its time complexity: to evaluate fib 5, we have to first:
- evaluate
fib 3, which callsfib 1and then callsfib 2(which callsfib 0andfib 1); then, - evaluate
fib 4, which callsfib 2(which callsfib 0andfib 1), and then callsfib 3, which callsfib 1and then callsfib 2(which callsfib 0andfib 1).
As you can see, the number of calls to fib n is exponential in n! This is no good.
Your job in this assignment is to use mutation to implement fib in a faster way.
Like Q3, we begin by setting up a functor:
module Fib (R : ManualRefSig) = struct
open R
module IntMap = Map.Make(Int)
(* Helper functions *)
let create_map (n : int) (f : int -> 'a) : 'a IntMap.t =
IntMap.of_list (List.init n (fun i -> (i, f i)))
(* Create a map using f, and return None if any calls to f return None. *)
let create_map_option (n : int) (f : int -> 'a option) : ('a IntMap.t) option =
let pairs = List.init n (fun i -> (i, f i)) in
if List.exists (fun p -> (Option.is_none (snd p))) pairs then
None
else
Some (IntMap.of_list (List.map (fun p -> (fst p, Option.get (snd p))) pairs))
let iter_map (m : 'a IntMap.t) (f : int -> 'a -> unit) : unit =
IntMap.iter f m
let fib_fast (a : int arena) (n : int) : int option = failwith "TODO"
end
You may implement fib_fast in any way you wish, with the following requirements:
- It must compute the same final answer as
fib, with the exception that if one of the functions inside ofR(e.g.,alloc / free / read / write) returnsNone, you should also returnNone. Given an arena with enough space,fib_fastshould returnSomewith the correct answer. fib_fastcannot itself be recursive onn. Instead, it can only usecreate_map/create_map_optionto create anIntMap; anditer_map, which allows you to traverse theIntMapin ascending order of the integer keys. (This can emulate aforloop, since you can create a map from 0 to N, and then iterate over it in ascending order.)fib_fastmust not usereforArray.t, and so on; but should instead use the functions inside ofRto manipulate mutable memory using the supplied arenaa. Additionally, you may not callfresh_arena. Thus, for this this problem, you may ONLY store integers inside ofmrefs.- Your code should not have memory leaks: all
mrefs you create withallocmust befreed before the function returns. - You may feel free to write helper functions, as long as these helper functions do not use recursion.
Hint
You can create an (int mref) IntMap.t (using create_map or create_map_option) which maps each index i to a fresh int mref which holds the value of fib i (initialized with -1, if that index hasn't been computed yet). Via create_map_option, you can return None if any allocation failed.
Then, you can compute the value of fib n by iterating over the map via iter_map.
Q4: Grading Criteria
Correctness and leakage-freedom of fib_fast (again, which is evaluated independently of the correctness of ManualRef from Q2).