This homework has three types of problems:
Self Check: You are strongly encouraged to think about and work through these questions, but you will not submit answers to them.
Problems: You will turn in answers to these questions.
Pair Programming: This part involves writing ML code. You are required to work with a partner on it. You are welcome to choose your own partner, but I can also assist in matching pairs --- simply send me a Slack message and I will pair you with someone else also looking for a partner. Please do not let finding a partner go until the last minute.
(Required) Read Mitchell, skim 4.4--4.5, 5, skim 6.1.
(As necessary) ML documentation and tutorials. These two may be particularly useful:
(Optional) J. Backus, Can programming be liberated from the von Neuman style?, Comm. ACM 21, 8 (1978) 613-641. You can find this on the cs334 web site.
Mitchell, Problem 6.1
Mitchell, Problem 4.11
The function g should be defined as follows (there is a typo in the book):
fun g(x, y) = if x = 0
then 1
else if x + y = 0
then 2
else 3;
Mitchell, Problem 5.1
Mitchell, Problem 4.6
For this problem, use the ML interpreter on the Unix machines in the computer lab, or on your own computer. See instructions here.
Your GitLab account will have a project for your to use for this
question. You can follow the same instructions as on HW 1 for
cloning it and adding a partner. You should answer the following in
the *.sml
files in your repository.
You can run ML on the file "example.sml" as follows:
sml < example.sml
at the command line. As with Lisp, the ML compiler will process the program in the file and print the result. For example, if "example.sml" contains
(* double an integer *)
fun double (x) = x * x;
(* return the length of a list *)
fun listLength (nil) = 0
| listLength (l::ls) = 1 + listLength ls
;
double (10);
listLength (1::[2,3,4]);
the command sml < example.sml
will produce the following:
val double = fn : int -> int
val listLength = fn : 'a list -> int
val it = 100 : int
val it = 4 : int
You can also run sml
and enter in declarations and expressions to
evaluate at the prompt.
Start early on this part so you can get in touch with me or the TAs if you have problems understanding the language. There are many valuable resources available to help you learn ML:
The examples in the Mitchell book and in your notes.
Several very good tutorials listed on the resources page. Of particular note:
A few additional details:
Emacs on the Unix machines will provide auto-formatting and syntax highlighting while editing ML files. Be sure your file names end with ".sml" so Emacs can recognize them as containing ML code.
Comments in ML are delineated by (*
and *)
.
Put the following line at the top of your ML files to ensure that large data types and lists are fully printed:
Control.Print.printDepth := 100;
Control.Print.printLength := 100;
Unless otherwise specified, you should use pattern matching where possible.
There are several thought questions in the descriptions below. Please answer these questions with your partner in comments in the code.
Define a function sumSquares
that, given a nonnegative integer
n, returns the sum of the squares of the numbers from 1 to n:
- sumSquares(4)
val it = 30 : int
- sumSquares(5)
val it = 55 : int
Define a function listDup
that takes an element, e, of any
type, and a non-negative number, n, and returns a list with n
copies of e:
- listDup("moo", 4);
val it = ["moo","moo","moo","moo"] : string list
- listDup(1, 2);
val it = [1,1] : int list
- listDup(listDup("cow", 2), 2);
val it = [["cow","cow"],["cow","cow"]] : string list list
Your function will have a type like
’a * int -> ’a list
. What does this type mean? Why is it the
appropriate type for your function. Answer this question as a
comment in the code.
Write the function zip
to compute the product of two lists of
arbitrary length. You should use pattern matching to define this
function:
- zip [1,3,5,7] ["a","b","c","de"];
val it = [(1,"a"),(3,"b"),(5,"c"),(7,"de")]: (int * string) list
Curry Your Function!
This is the curried version with type
’a list -> ’b list -> (’a * ’b) list
. Be sure to define it to
match this type.
Also, if the lists don't have the same length, you may decide how you would like the function to behave. If you don't specify any behavior at all you will get a "match not exhaustive" warning from the compiler to indicate that you have not taken care of all possible patterns--- this is fine.
Write the inverse function, unzip
, which behaves as follows:
- unzip [(1,"a"),(3,"b"),(5,"c"),(7,"de")];
val it = ([1,3,5,7], ["a","b","c","de"]): int list * string list
Write zip3
, to zip three lists.
- zip3 [1,3,5,7] ["a","b","c","de"] [1,2,3,4];
val it = [(1,"a",1),(3,"b",2),(5,"c",3),(7,"de",4)]: (int * string * int) list
Why can't you write a function zip_any
that
takes a list of any number of lists and zips them into tuples?
From the first part of this question it should be pretty clear
that for any fixed n, one can write a function zipn
. The
difficulty here is to write a single function that works for all
n. In other words, can we write a single function zip_any
such
that zip_any [list1,list2,...,listk]
returns a list of
k-tuples no matter what k is? Answer this question as a comment
in the code.
Write a function find with type ''a * ''a list -> int
that takes
a pair of an element and a list and returns the location of the
first occurrence of the element in the list. For example:
- find(3, [1, 2, 3, 4, 5]);
val it = 2 : int
- find("cow", ["cow", "dog"]);
val it = 0 : int
- find("rabbit", ["cow", "dog"]);
val it = ~1 : int
First write a definition for find where the element is
guaranteed to be in the list. Then, modify your definition so
that it returns ~1
if the element is not in the list.
You may wish to read this if you see a warning about polyEqual.
Here is the datatype definition for a binary tree storing integers at the leaves:
datatype IntTree = LEAF of int | NODE of (IntTree * IntTree);
Write a function sum:IntTree -> int
that adds up the values in
the leaves of a tree:
- sum(LEAF 3);
val it = 3 : int
- sum(NODE(LEAF 2, LEAF 3));
val it = 5 : int
- sum(NODE(LEAF 2, NODE(LEAF 1, LEAF 1)));
val it = 4 : int
Write a function height: IntTree -> int
that returns the
height of a tree:
- height(LEAF 3);
val it = 1 : int
- height(NODE(LEAF 2, LEAF 3));
val it = 2 : int
- height(NODE(LEAF 2, NODE(LEAF 1, LEAF 1)));
val it = 3 : int
Write a function balanced: IntTree -> bool
that returns true
if a tree is balanced (ie, both subtrees are balanced and differ
in height by at most one). You may use your height
function in
the definition of balanced
.
- balanced(LEAF 3);
val it = true : bool
- balanced(NODE(LEAF 2, LEAF 3));
val it = true : bool
- balanced(NODE(LEAF 2, NODE(LEAF 3, NODE(LEAF 1, LEAF 1))));
val it = false : bool
What is non-optimal about using the height
function in the definition of balanced
? Can you suggest a more
efficient implementation? You need not write code, but describe
in a sentence or two how you would do this. Answer this question
as a comment in the code.
Certain programming languages (and HP calculators) evaluate expressions using a stack. As I am sure many of you learned in cs136, PostScript is a programming language of this ilk for describing images when sending them to a printer. We are going to implement a simple evaluator for such a language. Computation is expressed as a sequence of operations, which are drawn from the following data type:
datatype OpCode =
PUSH of real
| ADD
| MULT
| SUB
| DIV
| SWAP
;
The operations have the following effect on the operand stack. (The top of the stack is shown on the left.)
OpCode | Initial Stack | Resulting Stack |
---|---|---|
PUSH(r) |
... | r ... |
ADD |
a b ... | (b + a) ... |
MULT |
a b ... | (b * a) ... |
SUB |
a b ... | (b - a) ... |
DIV |
a b ... | (b / a) ... |
SWAP |
a b ... | b a ... |
The stack may be represented using a list for this example, although we could also define a stack data type for it.
type Stack = real list;
Write a recursive evaluation function with the signature
eval : OpCode list * Stack -> real
It takes a list of operations and a stack. The function should perform each operation in order and return what is left in the top of the stack when no operations are left. For example,
eval([PUSH(2.0),PUSH(1.0),SUB],[])
returns 1.0. The eval
function will have the following basic
form:
fun eval (nil,a::st) = (* ... *)
| eval (PUSH(n)::ops,st) = (* ... *)
| (* ... *)
| eval (_,_) = 0.0
;
You need to fill in the blanks and add cases for the other opcodes.
The last rule handles illegal cases by matching any operation list and stack not handled by the cases you write. These illegal cases include ending with an empty stack, performing addition when fewer than two elements are on the stack, and so on. You may ignore divide-by-zero errors for now (or look at exception handling in the online resources -- we will cover that topic in a few weeks).
If you wrote a PostScript interpreter in cs136, compare that experience to this one. In particular, what advantages does ML offer for writing this type of program? (No need to write an answer to this question, but come ready to talk about it at the next lecture).
Submit your homework via GradeScope by the beginning of class on the due date.
Submit your answers to the Gradescope assignment named, for example, "HW 1". It should:
You will be asked to resubmit homework not satisfying these requirements. Please select the pages for each question when you submit.
If this homework includes programming problems, submit your code to the Gradescope assignment named, for example, "HW 1 Programming". Also:
Autograding: For most programming questions, Gradescope will run an autograder on your code that performs some simple tests. Be sure to look at the autograder output to verify your code works as expected. We will run more extensive tests on your code after the deadline. If you encounter difficulties or unexpected results from the autograder, please let us know.