Module Specifications.Assign5

This assignment is due on Thursday 3/6 by 8:00PM. You should put all of your solutions in assign5/lib/assign5.ml. See the file test/test_assign5.ml for example behavior of each function.

Programming

Practice Problems (Ungraded)

These problems come the textbook OCP.

Evaluation with Errors

In this problem we'll be looking at a common pattern for representing expressions with metadata. We'll look at arithmetic expressions with addition, subtraction, multiplication, division, and exponentiation.

Expressions are defined mutually recursively so that all expressions, including subexpressions, carry metadata of a given type parameter.

type op =
  1. | Add
  2. | Sub
  3. | Mul
  4. | Div
  5. | Pow
type 'a expr = {
  1. expr : 'a _expr;
  2. meta : 'a;
}
and 'a _expr =
  1. | Num of int
  2. | Op of op * 'a expr * 'a expr

Note the very cool use of the and keyword, which allows use to define mutually recursive types (and functions). We'll be implementing an evaluator for expressions with metadata, and for which errors cases are handled using the result type.

There are two kinds of errors that could occur when evaluating an arithmetic expression of this form:

Like expressions, errors carry metadata. As an example, we can imagine expressions and errors carrying data describing the line number at which an expression/error occurs, so that we can present a better error message to the user.

type error_kind =
  1. | DivByZero
  2. | NegExp
type 'a error = {
  1. error : error_kind;
  2. meta : 'a;
}
val eval : 'a expr -> (int, 'a error) Stdlib.result

Implement the function eval so that eval e is Ok n if e has no division-by-zero or negative-exponent errors and has the value n. The expression eval e should be Error {error;meta} otherwise, where error describes the kind of error in e and meta is the metadata of the leftmost division expression whose right-hand side evaluates to zero, or the leftmost exponentiation expression whose right-hand side evaluates to a negative number.

You can implement this function however you'd like but we recommend trying to use the monadic let* notation, since the solution done in this way is quite clean.

We've included the function guard, which may be helpful. It can be used as follows:

let ( let* ) = Result.bind

let guard b error = if b then Error error else Ok ()

let check x_res error =
  let* x = x_res in
  let* _ = guard (x = 0) error in
  Ok x

let _ = assert (check (Ok 2) () = Ok 2)
let _ = assert (check (Error ()) () = Error ())
let _ = assert (check (Ok 0) () = Error ())

Exception Handling

exception ListTooShort
exception InvalidArg
val prefix : int -> 'a list -> 'a list

Implement the function prefix so that prefix k l is the list consisting of the first k elements of l. It should raise a ListTooShort exception if k is greater than the length of l, and should raise an InvalidArg exception if k is negative.

type prefix_error =
  1. | ListTooShort
  2. | InvalidArg
val prefix_res : int -> 'a list -> ('a list, prefix_error) Stdlib.result

Implement the function prefix_res so that prefix k l is Ok p where p is the list consisting of the first k elements of l, given k is at most the length of l and k is nonnegative. It should return an error otherwise (I'll let you determine what kind of error). Hint: Don't reimplement the function.

Deques

A double-ended queue, or dequeue, is a list-like data structure for which is possible to pop from and push to both the front and back. For our purposes a dequeue implements the following signature.

module type DEQUEUE = sig ... end
module ListDequeue : DEQUEUE with type 'a t = 'a list
module DoubleListDequeue : DEQUEUE with type 'a t = 'a list * 'a list

You need to implement two modules with this signature.

For the DoubleListDequeue implementation, we keep track of a front and back list, which we can pop from and push to. This means the the back list holds elements in reverse. In the case that the front list is empty when pop_front is called, the front and back list should be balanced. This means front and back after the call should have the property that

List.length back - List.length front <= 1

The same is true for a call to pop_back, the roles of front and back reversed. For example:

front: []                     front: [1;2;3;4;5;6]
 back: [5;4;3;2;1]             back: []

⇓ pop_front                   ⇓ pop_back

front: [2;3]                  front: [1;2;3]
 back: [5;4]                   back: [5;4]

Mapping Keys

module StringMap : Stdlib.Map.S with type key = string
module IntMap : Stdlib.Map.S with type key = int
module StringSet : Stdlib.Set.S with type elt = string

In the second half of the course, we'll make use of maps and sets, which are implemented in OCaml using module functors. In the case of maps, we create a module for the type of we want to use for keys. For example, we use Map.Make to create structures for maps with a given type of key, e.g., StringMap and IntMap for maps with string keys and int keys, respectively. In the case of sets, we use Set.Make to create structures for sets with a given type of element.

val flip_keys_and_values : int StringMap.t -> StringSet.t IntMap.t

Implement the function flip_keys_and_values so that flip_keys_and_values m is the map m' gotten by making the value of m into keys. The value associated with the key v in m' is the set of keys in m which map to the value v.

This function will require looking in the the OCaml standard library (not stdlib320) on Maps and Sets.

Written

Partial Derivation

Not all judgments are derivable, but it's possible to write partial derivations of underivable judgments. These partial derivations can give us insight into why we get certain type errors when type-checking.

Formally, a typing derivation is partial if its leaves are not necessarily axioms. A partial derivation is maximal if every leaf of the derivation is either an axiom or a judgment which cannot be derived by any rule in our specification from any judgments.

Give maximal partial derivations of the following typing judgments.

    \varnothing \vdash \texttt{if true then 1 + 3 else x / false} : \texttt{int}
    
    \varnothing \vdash \texttt{if true then 1 + 3 else false / x} : \texttt{int}
    

In addition, write down the type error which OCaml gives you when trying to type-check each expression. What does the type-error tell you about the order in which OCaml type-checks sub-expressions?

Typing Derivation

So far, we haven't introduced any rules for typing recursive functions. Here's such a rule.

    \frac
    {\Gamma, f : \tau \to \tau_1, x : \tau \vdash e_1 : \tau_1
     \qquad
     \Gamma, f : \tau \to \tau_1 \vdash e_2 : \tau_2
    }
    {\Gamma \vdash \texttt{let rec} \ f \ x \ \texttt{=} \ e_1 \ \texttt{in} \ e_2 : \tau_2}
    

Note that this rule puts f into the context when determining the type of e_1, the body of f, which means that f can appear in its own body (and, hence, may be recursive). Using this rule, give a derivation of the following typing judgment.

    \varnothing \vdash \texttt{let rec sum l = match l with | [] -> 0 | h :: t -> h + sum t in sum} : \texttt{int → int}
    

This will be a somewhat wide derivation. You may use arrows as we've done in lecture to make the derivation less wide, but please make sure your solution is very clear.

Semantic Derivation

    \texttt{match 1 :: 2 :: [] with | [] -> true | h :: t -> false} \Downarrow \bot
    

Give a derivation of the above semantic judgment.