~cs538-1/public/handin/proj2/login
where login
is your login name.1,
2a, 2b, 2c, 3a, 3b, 4a, 4b, 4c
. You can also include a file called
README
with any notes about your submission, including the timing results for part 3b. PLEASE NOTE: name your files 1, 2a, 2b ...
without an extension and not 1.sml, 2a.sml ...
or anything else. Put all the files in the handin directory and do not make separate subdirectories for each question.
2a
in your solution for 2b
), and don't assume
that your code will execute in the handin folder. To achieve this, you may
need to make copies of code that is common to multiple sub-parts of the same
question. E.g., copy the definition of listify
from file 2a
into file 2b
.
Also make sure to define your types, functions, values, exceptions to be EXACTLY the same type as shown here, and they should behave in EXACTLY the same way as shown below. Functions should be of the right type and take arguments in the right order, e.g., function enter
in question 1 should take a single tuple of 3 elements (priority, value, queue in that order) as its single argument rather than 3 separate arguments. Furthermore, 'priority' should be an integer, 'value' can be any type T, and 'queue' should be of type T PriorityQ (matching the type T of 'value').
Also, be sure to see below the notes and hints for each question.
If you are unclear about the types or format or purpose of function arguments and return values, please clarify the matter with the instructor or TA before submitting the code.
Question | Required definitions | Sample usage |
---|---|---|
1 |
type 'a PriorityQ |
- val q1 = nullQueue : string PriorityQ;
|
2a |
val listify = fn int list -> int list list
|
- listify [1,2,3,10,5,7,6];
|
2b |
val duplicates = fn int list -> bool
|
- duplicates [1,2,3,4,5,6];
|
2c |
val duplicates2 = fn int list -> bool
|
- duplicates2 [1,2,2,3,4,5,6];
|
3a |
val f = fn int -> int
|
- f 5;
|
3b |
val fastF = fn int -> int
|
- fastF 5;
|
4a |
datatype 'a lazyList = cons of 'a * (unit -> 'a lazyList) | nullList
|
- val s1 = seq(10,400);
|
4b |
datatype 'a lazyList = cons of 'a * (unit -> 'a lazyList) | nullList
|
- val c = cons(false, fn () => cons(true, fn () => nullList));
|
4c |
datatype 'a lazyList = cons of 'a * (unit -> 'a lazyList) | nullList
|
- val p = primes();
|
/s/std/bin/sml
).
You are free to work on your own with a newer version of SML which you might
have downloaded and installed at home but before submission,
you must make sure your code runs with our version of SML.
If you use SML library functions, please make sure that these library functions
are also available in the SML/NJ installed here in CS.
print
or other SML output functions. Each of the functions listed
above should silently return values as required by the question. Also,
please make sure that your files are free of any top-level tests or timings
that you may have used during testing and debugging. This extra code can
confuse the automatic tester we use.
No extra points for the most elegant programs but you are encouraged to write
in a functional style, making best use of SML's natural abstractions:
lists, pattern-matching, recursion, higher-order functions, and so on.
These will invariably reward you with compact, natural-looking code which
is easy to read and understand. See the lecture notes and look online for
SML programs to get a flavor of ML programming.
We will penalize programs that are excessively kludgy and hard to read or
understand. Don't try to write C++ or Java in SML. In particular, the
excessive use of the reference operators (ref
and !
and !=
and their cousins) is frowned upon and
will be penalized. Note that this does not apply when an imperative
update using references might be required or natural, as with memoization
in question 3(b).
Partial credit will be given for programs that seem to be on the right track.
Also see the SML/NJ home page for latest information on the language and available libraries (including the Standard Basis library).
SML/NJ has strong static typing, forcing you to make your program type-correct before it can be run (leading to a slogan of ML fans: "if it type-checks, it just runs"). But its error messages can be very cryptic leading to much time being wasted trying to figure out what SML is trying to tell you. Style note: it is better to use SML's type inference system and not put explicit type annotations everywhere.
When in doubt, parenthesize. It's a common error to omit parentheses around parameters in patterns, e.g., a list pattern should be written as (h::t)
and not just h::t
. Also, each definition requires you to use the val
or fun
keywords, even definitions within a let
clause.
When pattern matching, make sure you cover all combinations of possibilities for arguments. You may not care about all combinations, and some of them might be impossible, but the compiler does not know that. It will warn you of non-exhaustive matches if your pattern-matching clauses are not sufficient.
Also, it is preferable to use pattern matching rather than tuple selectors like #1 or #2, or list selectors like hd or tl. In some cases, using tuple selectors might cause type errors (the inscrutable unresolved flex record) since there is not sufficient information for SML to deduce the exact number of elements in a tuple. In such cases, you should explicitly specify the size of the tuple in the pattern, e.g., write
fun f (a,b) = brather than
fun f t = #2(t)
See this page for some help with deciphering SML error messages, and see the SML/NJ errors list for the definitive reference.
As an example, consider a standard (non-priority) queue which we implement as a list with a head and a tail pointer (indices into the list). The type we expose to the outside world is the type variable 'a, the type of the elements in the queue, and declared in the first line below. The representation type is declared in the second line, and is a tuple of 2 integers (head and tail indices) and a list of elements of the same type 'a. Note that the representation type can have completely arbitrary structure, of our choosing, as long as it is linked to the external type via the type variable 'a. It is hidden from the outside world and is only available to our queue functions. The outside world only sees values of type
Assignment Notes, Hints, FAQ
Question 1.
abstype 'a stack
defined in class. We're not looking for the most efficient implementation, rather more for getting the functionality and for defining the abstract type in the right way, hiding the representation.
(int * 'a) PriorityQ
, or other representation-specific types. Your functions should handle priority queues parameterized by a single type 'a: the type of the value that can be put in the queue, as shown in the sample usage above for 'a = string
. Your queues should not make the internal representation of the queue (e.g., tuples of integer priority and queue value) visible to the outside world. 'a PriorityQ
is an ABSTRACT DATA TYPE and hence hides its internal representation. Consumers of PriorityQ
are only aware of one type parameter 'a which is the type of values that can reside in the priority queue. But consumers are not, and should not be, aware of the internal representation. It should be possible to substitute another PriorityQ
which has the same type but a completely different representation and still have everything work.
int queue
, string queue
and so on, while our queue functions see values of type int list * int * int
, string list * int * int
and so on.
abstype 'a queue =
Q of 'a list * int * int
with
val enqueue(Q(l,h,t)) = ...
fun dequeue(Q(l,h,t)) = ...
... etc ...
end;
q1
is given an
explicit type of string PriorityQ
when it is assigned
the null queue.
Question 2.
duplicates
and
duplicates2
take ordinary lists of integers
as input and check for duplicates. They don't take a
listified list as input, they call
listify
internally. Please see the
sample usage above.
Question 3.
f
in a table (i.e., a cache of function values), and then look up the table before you recompute the same values in the future. Your task is to figure out how to maintain this table inside the function fastF
. You will not be graded by speed (as long you don't submit something that's as slow as the original function f
!), but by how you implemented the memo table.
ref, !, :=
, which are allowed in Q. 3b) to maintain a hidden table that remembers the return values across calls to function f
. This will probably be the fastest implementation. The other method does not use imperative updates, and only remembers computed values in a table within a single sequence of recursive calls to f
. Either way is fine for this question, and will receive full points.
Call it using
fun time f n =
let
val t = Timer.startCPUTimer()
val v = f n
val usrsecs = Time.toReal(#usr(Timer.checkCPUTimer(t)))
in
print(Real.toString(usrsecs));
v
end;
time f 28
or time fastF 28
. Repeat measurements a few times to get a stable average.
Question 4.
let fun f = x+2 in (f,0) end;
or by using an anonymous function:
( (fn x => (x+2)), 0 )
. Both of these evaluate to a tuple of a
function and integer zero. Functions of no arguments are defined using ()
(of type unit
) as the argument.
primes
, it may be helpful to break the task
into sub-tasks, and first define a function that can filter multiples of a given integer from a lazy list of integers. Then think about how you can use this to generate the lazy list of primes.
- Manoj Plakal <plakal@cs.wisc.edu>