Due Wednesday, April 6 at 11:59PM.
In this assignment you will implement Hindley-Milner type inference, which represents the current ``best practice'' for flexible static typing. The assignment has two purposes:
Make a clone of the book code:
git clone linux.cs.tufts.edu:/comp/105/build-prove-compare
The code you need is in bare/nml/ml.sml.
Working on your own, please solve Exercise 1 on page 494 and Exercise 2 on page 494 of Ramsey. These exercises explore some implications of type inference.
1. Exploring the meaning of polymorphic types I. Do Exercise 1 on page 494 of Ramsey.
2. Exploring the meaning of polymorphic types II. Do Exercise 2 on page 494 of Ramsey.
Working with a partner, please solve 16, 17 and 18 from pages 497–497 of Ramsey, and the two exercises S and T below.
16. Implementing a constraint solver. Do Exercise 16 on page 497 of Ramsey. This exercise is probably the most difficult part of the assignment. Before proceeding with type inference, make sure your solver produces the correct result on our test cases and on your test cases. You may also want to show your solver code to the course staff before proceeding to type inference.
17. Implementing type inference. Do Exercise 17 on page 497 of Ramsey.
18. Adding primitives. Do Exercise 18 on page 497 of Ramsey.
S. Test cases for the solver. Write three test cases for the constraint solver. At least two of these test cases should be constraints that have no solution. Assuming that we provide a function constraintTest : con -> answer, write your test cases as three successive calls to constraintTest. Do not define constraintTest yourself.
Here is a sample set of test cases:
val _ = constraintTest (TYVAR "a" ~ TYVAR "b") val _ = constraintTest (CONAPP (TYCON "list", [TYVAR "a"]) ~ TYCON "int") val _ = constraintTest (TYCON "bool" ~ TYCON "int")
Naturally, you will supply your own test cases, different from these.
T. Test cases for type inference. Write three test cases for type inference. At least two of these test cases should be for terms that fail to type check. Each test case should be a definition written in nML. Here is a sample set of test cases:
(val weird (lambda (x y z) (cons x y z))) (+ 1 #t) (lambda (x) (cons x x))
Naturally, you will supply your own test cases, different from these.
For extra credit, you may complete any of the following:
Of these exercises the most interesting are probably Mutation (easy) and Explicit types (not easy).
You should submit two files:
When you are ready, run submit105-ml-inf-solo to submit your work. Note you can run this script multiple times; we will grade the last submission.
You should submit four files:
When you are ready, run submit105-ml-inf-pair to submit your work. Note you can run this script multiple times; we will grade the last submission.
This is one assignment where it pays to run a lot of tests, of both good and bad definitions. The most effective test of your algorithm is not that it properly assign types to correct terms, but that it reject ill-typed terms. I have posted a functional topological sort that makes an interesting test case.
If you call your interpreter ml.sml, you can build a standalone version in a.out by running mosmlc ml.sml or a faster version in ml by running mlton -output a.out ml.sml. To help you with the solver, once you have implemented solve, the following code redefines solve into a version that checks itself for sanity (ie, idempotence). It is a good idea to check that the substitution returned by your solver is idempotent before using it in your type inferencer.
fun isStandard pairs = let fun distinct a' (a, tau) = a <> a' andalso not (member a' (freetyvars tau)) fun good (prev', (a, tau)::next) = List.all (distinct a) prev' andalso List.all (distinct a) next andalso good ((a, tau)::prev', next) | good (_, ) = true in good (, pairs) end val solve = fn c => let val theta = solve c in if isStandard theta then theta else raise BugInTypeInference "non-standard substitution" end
In writing the type-inference code, you should refer to the
typing rules of nml, which appear on pages 500-501 of Ramsey.
With your solver in place, the type inference should be
straightforward, with two exceptions:
You can emulate the implementations for
val-rec, but you must split the
constraint into local and global portions.
The splitting is covered in detail in the book, on pages
469–472, with particular focus in the sidebar on page 470.
The course interpreter is located in /comp/105/bin/nml. If your interpreter can process the initial basis and infer correct types, you are doing OK.
The real test of your interpreter is that it should reject incorrect definitions. You should prepare a dozen or so definitions that should not type check, and make sure they don't. For example:
(val bad (lambda (x) (cons x x))) (val bad (lambda (x) (cdr (pair x x))))
Pick your toughest three test cases to submit for Exercise T.
Here some common mistakes:
NILand one case for
There are also some common assumptions which are mistaken:
beginis never empty.
Your solutions are going to be evaluated automatically. We must be able to compile your solution in Moscow ML by typing, e.g.,
If there are errors or warnings in this step, your work will earn No Credit for functional correctness.
We will focus most of our evaluation on your constraint solving and type inference.
• The code has no offside violations.
• Or, the code has just a couple of minor offside violations.
• Indentation is consistent everywhere.
• The submission has no bracket faults.
• The submission has a few minor bracket faults.
• Or, the submission has no bracketed names, but a few bracketed conditions or other faults.
• The code has several offside violations, but course staff can follow what's going on without difficulty.
• In one or two places, code is not indented in the same way as structurally similar code elsewhere.
• The submission has some redundant parentheses around function applications that are under infix operators (not checked by the bracketing tool)
• Or, the submission contains a handful of bracketing faults.
• Or, the submission contains more than a handful of bracketing faults, but just a few bracketed names or conditions.
• Offside violations make it hard for course staff to follow the code.
• The code is not indented consistently.
• The submission contains more than a handful of parenthesized names as in
• The submission contains more than a handful of parenthesized
• Type variables have names beginning with
• Types, type variables, constraints, and substitutions mostly respect conventions, but there are some names like
• Some names misuse standard conventions; for example, in some places, a type variable might have a name beginning with
• The nine cases of simple type equality are handled by these five patterns:
• The code for solving α ∼ τ has exactly three cases.
• The constraint solver is implemented using an appropriate set of helper functions, each of which has a good name and a clear contract.
• Type inference for list literals has no redundant case analysis.
• Type inference for expressions has no redundant case analysis.
• In the code for type inference, course staff see how each part of the code is necessary to implement the algorithm correctly.
• Wherever possible appropriate, submission uses
• The nine cases are handled by nine patterns: one for each pair of value constructors for
• The code for α ∼ τ has more than three cases, but the nontrivial cases all look different.
• The constraint solver is implemented using too many helper functions, but each one has a good name and a clear contract.
• The constraint solver is implemented using too few helper functions, and the course staff has some trouble understanding the solver.
• Type inference for list literals has one redundant case analysis.
• Type inference for expressions has one redundant case analysis.
• In some parts of the code for type inference, course staff see some code that they believe is more complex than is required by the typing rules.
• Submission sometimes uses a fold where
• The case analysis for a simple type equality does not have either of the two structures on the left.
• The code for α ∼ τ has more than three cases, and different nontrivial cases share duplicate or near-duplicate code.
• Course staff cannot identify the role of helper functions; course staff can't identify contracts and can't infer contracts from names.
• Type inference for list literals has more than one redundant case analysis.
• Type inference for expressions has more than one redundant case analysis.
• Course staff believe that the code is significantly more complex than what is required to implement the typing rules.
• Submission includes one or more recursive functions that could have been written without recursion by using
Back to the class home page