COMP 105 Homework: Core ML

Due Monday, March 9, at 11:59 PM
You are strongly encouraged not to take a token on this assignment because the midterm is on Wednesday, March 11.

The purpose of this assignment is to help you get acclimated to programming in ML:

By the time you complete this assignment, you will be ready to tackle serious programming tasks in core ML.

You will be more effective if you are aware of the Standard ML Basis Library. Jeff Ullman's text (Chapter 9) describes the 1997 basis, but today's compilers use the 2004 basis, which is a standard. You will find a few differences in I/O, arrays, and elsewhere; the most salient difference is in TextIO.inputLine.

The most convenient guide to the basis is the Moscow ML help system; type

- help "";
at the mosml interactive prompt. The help file displays an incorrect date in its banner, but as far as I know, it is up to date.

Dire warnings

There are some functions and idioms that you must avoid. Code violating any of these guidelines will earn No Credit.

Other guidelines

Some useful list patterns include these patterns, to match lists of exactly 0, 1, 2, or 3 elements:
<patterns>= [D->]
[]
[x]
[x, y]
[a, b, c]

and also these patterns, which match lists of at least 0, 1, 2, or 3 elements:

<patterns>+= [<-D]
xs
x::xs
x1::x2::xs
a::b::c::xs

When using these patterns, remember that function application has higher precedence than any infix operator! This is as true in patterns as it is anywhere else.

Use the standard basis extensively. Moscow ML's help "lib"; will tell you all about the library. And if you use

ledit mosml -P full
as your interactive top-level loop, it will automatically load almost everything you might want from the standard basis.

All the sample code we show you is gathered in one place online.

As you learn ML, this table may help you transfer your knowledge of μScheme:

μSchemeML
val val
definefun
lambdafn

Put all your solutions in one file: warmup.sml. (If separate files are easier, combine them with cat.) At the start of each problem, please label it with a short comment, like

(***** Problem A *****)
To receive credit, your warmup.sml file must compile and execute in the Moscow ML system. For example, we must be able to compile your code without warnings or errors:
% /usr/sup/bin/mosmlc -c warmup.sml
%
Please remember to put your name and the time you spent in the warmup.sml file.

Proper ML style

Ullman provides a gentle introduction to ML, and his book is especially good for programmers whose primary experience is in C-like languages. But, to put it politely, Ullman's ML is not idiomatic. Much of what you see in Ullman should not be imitated. The textbook by Ramsey is a better guide to what ML should look like. We also direct you to these resources: In the long run, we expect you to master and follow the guidelines in the style guide.

A collection of problems to be solved individually (90%)

Working on your own, please solve the following problems:

Higher-order programming

  1. Here's a function that is somewhat like fold, but it works on binary operators.

    1. Define a function
      val compound : ('a * 'a -> 'a) -> int -> 'a -> 'a
      
      that ``compounds'' a binary operator rator so that compound rator n x is x if n=0, x rator x if n = 1, and in general x rator (x rator (... rator x)) where rator is applied exactly n times. compound rator need not behave well when applied to negative integers.

    2. Use the compound function to define a Curried function for integer exponentiation
      val exp : int -> int -> int
      
      so that, for example, exp 3 2 evaluates to 9. Hint: take note of the description of op in Ullman S5.4.4, page 165.

    Don't get confused by infix vs prefix operators. Remember this:

    My estimate of difficulty: Mostly easy. You might be troubled by an off-by-one error.


Patterns

  1. Write a function firstVowel that takes a list of lower-case letters and returns true if the first character is a vowel (aeiou) and false if the first character is not a vowel or if the list is empty. Use the wildcard symbol _ whenever possible, and avoid if. Remember that the ML character syntax is #"x", as decribed in Ullman, page 13.
    My estimate of difficulty: Easy to medium

  2. Write the function null, which when applied to a list tells whether the list is empty. Avoid if, and make sure the function takes constant time. Make sure your function has the same type as the null in the Standard Basis.
    My estimate of difficulty: Easy

Lists

  1. foldl and foldr are predefined with type
    ('a * 'b -> 'b) -> 'b -> 'a list -> 'b
    
    They are like the μScheme versions except the ML versions are Curried.
    1. Implement rev (the function known in μScheme as "reverse") using foldl or foldr.
    2. Implement minlist, which returns the smallest element of a non-empty list of integers. Your solution should work regardless of the representation of integers (e.g., it should not matter how many bits are used to represent integers). Your solution can fail (e.g., by raise Match) if given an empty list of integers. Use foldl or foldr.
    Do not use recursion in any of your solutions.

    My estimate of difficulty: Medium

  2. Implement foldl and foldr using recursion. Do not create unnecessary cons cells. Do not use if.

    My estimate of difficulty: Easy

  3. Define a function
    val pairfoldr : ('a * 'b * 'c -> 'c) -> 'c -> 'a list * 'b list -> 'c
    
    that applies a three-argument function to a pair of lists of equal length, using the same order as foldr.

    Use pairfoldr to implement zip.

    My estimate of difficulty: Easy

  4. Define a function
    val flatten : 'a list list -> 'a list
    
    which takes a list of lists and produces a single list containing all the elements in the correct order. For example,
    <sample>+= [<-D->]
    - flatten [[1], [2, 3, 4], [], [5, 6]];
    > val it = [1, 2, 3, 4, 5, 6] : int list
    

    To get full credit for this problem, your function should use no unnecessary cons cells.

    My estimate of difficulty: Easy

Exceptions

  1. Write a (Curried) function
    val nth : int -> 'a list -> 'a
    
    to return the nth element of a list. (Number elements from 0.) If nth is given arguments on which it is not defined, raise a suitable exception. You may define one or more suitable exceptions or you may choose to use an appropriate one from the initial basis. (If you have doubts about what's appropriate, play it safe and define an exception of your own.)

    I expect you to implement nth yourself and not simply call List.nth.

    My estimate of difficulty: Easy

  2. Environments
    1. Define a type 'a env and functions
      <sample>+= [<-D->]
      type 'a env = (* you fill in this part *)
      exception NotFound of string
      val emptyEnv : 'a env = (* ... *)
      val bindVar : string * 'a * 'a env -> 'a env = (* ... *)
      val lookup  : string * 'a env -> 'a = (* ... *)
      
      Defines bindVar, emptyEnv, env, lookup, NotFound (links are to index).

      such that you can use 'a env for a type environment or a value environment. On an attempt to look up an identifier that doesn't exist, raise the exception NotFound. Don't worry about efficiency.

    2. Do the same, except make type 'a env = string -> 'a, and let
      <sample>+= [<-D->]
      fun lookup (name, rho) = rho name
      
      Defines lookup (links are to index).

    3. Write a function
      val isBound : string * 'a env -> bool
      
      that works with both representations of environments. That is, write a single function that works regardless of whether environments are implemented as lists or as functions. You will need imperative features, like sequencing (the semicolon). Don't use if.
    4. Write a function
      val extendEnv : string list * 'a list * 'a env -> 'a env
      
      that takes a list of variables and a list of values and adds the corresponding bindings to an environment. It should work with both representations. Do not use recursion. Hint: you can do it in two lines using the higher-order list functions defined above.

    You shoud put these functions in the same file; don't worry about the later ones shadowing the earlier ones.

    My estimate of difficulty: Medium, because of those pesky functions as values again

Algebraic data types

  1. Search trees.
    ML can easily represent binary trees containing arbitrary values in the nodes:
    <sample>+= [<-D->]
    datatype 'a tree = NODE of 'a tree * 'a * 'a tree 
                     | LEAF
    
    Defines tree (links are to index).

    To make a search tree, we need to compare values at nodes. The standard idiom for comparison is to define a function that returns a value of type order. As discussed in Ullman, page 325, order is predefined by

    <sample>+= [<-D->]
    datatype order = LESS | EQUAL | GREATER     (* do not include me in your code *)
    
    Defines order (links are to index).

    Because order is predefined, if you include it in your program, you will hide the predefined version (which is in the initial basis) and other things may break mysteriously. So don't include it.

    We can use the order type to define a higher-order insertion function by, e.g.,

    <sample>+= [<-D->]
    fun insert cmp =
        let fun ins (x, LEAF) = NODE (LEAF, x, LEAF)
              | ins (x, NODE (left, y, right)) = 
                  (case cmp (x, y)
                    of LESS    => NODE (ins (x, left), y, right)
                     | GREATER => NODE (left, y, ins (x, right))
                     | EQUAL   => NODE (left, x, right))
        in  ins
        end
    
    Defines insert (links are to index).

    This higher-order insertion function accepts a comparison function as argument, then returns an insertion function. (The parentheses around case aren't actually necessary here, but I've included them because if you leave them out when they are needed, you will be very confused by the resulting error messages.)

    We can use this idea to implement polymorphic sets in which we store the comparison function in the set itself. For example,

    <sample>+= [<-D->]
    datatype 'a set = SET of ('a * 'a -> order) * 'a tree
    fun nullset cmp = SET (cmp, LEAF)
    
    Defines nullset, set (links are to index).

    My estimate of difficulty: Easy to Medium

An immutable, persistent alternative to linked lists

  1. For this problem I am asking you to define your own representation of a new abstraction: the list with finger. A list with finger is a nonempty sequence of values, together with a ``finger'' that points at one position in the sequence. The abstraction provides constant-time insertion and deletion at the finger.

    This is a challenge problem. The other problems on the homework all involve old wine in new bottles. To solve this problem, you have to think of something new.

    1. Define a representation for type 'a flist. (Before you can define a representation, you will want to study the rest of the parts of this problem, plus the test cases.)

      Document your representation by saying, in a short comment, what sequence is meant by any value of type 'a flist.

    2. Define function
      val singletonOf : 'a -> 'a flist
      
      which returns a sequence containing a single value, whose finger points at that value.

    3. Define function
      val atFinger : 'a flist -> 'a
      
      which returns the value that the finger points at.

    4. Define functions
        val fingerLeft  : 'a flist -> 'a flist
        val fingerRight : 'a flist -> 'a flist
      
      Calling fingerLeft xs creates a new list that is like xs, except the finger is moved one position to the left. If the finger belonging to xs already points to the leftmost position, then fingerLeft xs should raise the same exception that the Basis Library raises for array access out of bounds. Function fingerRight is similar. Both functions must run in constant time and space.

      Please think of these functions as "moving the finger", but remember no mutation is involved. Instead of changing an existing list, each function creates a new list.

    5. Define functions
        val deleteLeft  : 'a flist -> 'a flist
        val deleteRight : 'a flist -> 'a flist
      
      Calling deleteLeft xs creates a new list that is like xs, except the value x to the left of the finger has been removed. If the finger points to the leftmost position, then deleteLeft should raise the same exception that the Basis Library raises for array access out of bounds. Function deleteRight is similar. Both functions must run in constant time and space. As before, no mutation is involved.

    6. Define functions
        val insertLeft  : 'a * 'a flist -> 'a flist
        val insertRight : 'a * 'a flist -> 'a flist
      
      Calling insertLeft (x, xs) creates a new list that is like xs, except the value x is inserted to the left of the finger. Function insertRight is similar. Both functions must run in constant time and space. As before, no mutation is involved. (These functions are related to "cons".)

    7. Define functions
        val ffoldl : ('a * 'b -> 'b) -> 'b -> 'a flist -> 'b
        val ffoldr : ('a * 'b -> 'b) -> 'b -> 'a flist -> 'b
      
      which do the same thing as foldl and foldr, but ignore the position of the finger.

    Here is a simple test case, which should produce a list containing the numbers 1 through 5 in order. You can use ffoldr to confirm.

      val test = singletonOf 3
      val test = insertLeft  (1, test)
      val test = insertLeft  (2, test)
      val test = insertRight (4, test)
      val test = fingerRight test
      val test = insertRight (5, test)
    
    You'll want to test the delete functions as well.

    Hints: The key is to come up with a good representation for "list with finger." Once you have a good representation, the code is easy: over half the functions can be implemented in one line each, and no function requires more than two lines of code.

    My estimate of difficulty: Hard

One problem you can do with a partner (10%)

The goal of this problem is to give you practice working with an algebraic data type that plays a central role in programming languages: expressions. In the coming month, you will write many functions that consume expressions; this problem will help you get off to a good start. It will also give you a feel for the kinds of things compiler writers do.

This problem asks you to explore the fact that a compiler doesn't need to store an entire environment in a closure; it only needs to store the free variables of its lambda expression. For the details you will want the handout on free variables, which is also available as Section 5.9 in your textbook.

You'll solve the problem in a prelude and four parts:

Hints:

My implementation of freeIn is 21 lines of ML.
My estimate of difficulty: Understanding free variables is hard, but once you understand, the coding is easy.

Extra credit

VARARGS

Extend μScheme to support procedures with a variable number of arguments. Do so by giving the name ... (three dots) special significance when it appears as the last formal parameter in a lambda. For example:
-> (val f (lambda (x y ...)) (+ x (+ x (foldl + 0 ...)))
-> (f 1 2 3 4 5) ; inside f, rho = { x |-> 1, y |->, ... |-> '(3 4 5) }
15
In this example, it is an error for f to get fewer than two arguments. If f gets at least two arguments, any additional arguments are placed into an ordinary list, and the list is used to initialize the location of the formal parameteter associated with ....
  1. Implement this new feature inside of mlscheme.sml. I recommend that you begin by changing the definition of lambda on page 225 to
       and lambda = name list * { varargs : bool } * exp
    
    The type system will tell you what other code you have to change. For the parser, you may find the following function useful:
      fun newLambda (formals, body) =
         case rev formals
           of "..." :: fs' => LAMBDA (rev fs', {varargs=true},  body)
            | _            => LAMBDA (formals, {varargs=false}, body)
    
    The type of this function is
    name list * exp -> name list * {varargs : bool} * exp;
    thus it is designed exactly for you to adapt old syntax to new syntax; you just drop it into the parser wherever LAMBDA was used.

  2. As a complement to the varargs lambda, write a new call primitive such that
    (call f '(1 2 3))
    is equivalent to
    (f 1 2 3)
    Sadly, you won't be able to use PRIMITIVE for this; you'll have to invent a new kind of thing that has access to the internal eval.

  3. Demonstrate these utilities by writing a higher-order function cons-logger that counts cons calls in a private variable. It should operate as follows:
    -> (val cl (cons-logger))
    -> (val log-cons (car cl))
    -> (val conses-logged (cdr cl))  
    -> (conses-logged)
    0
    -> (log-cons f e1 e2 ... en) ; returns (f e1 e2 ... en), incrementing
                                 ; private counter whenever cons is called
    -> (conses-logged)
    99  ; or whatever else is the number of times cons is called 
        ; during the call to log-cons
    

  4. Rewrite the APPLY-CLOSURE rule to account for the new abstract syntax and behavior. To help you, simplified LaTeX for the original rule is online.

Make sure your solutions have the right types

On this assignment, it is a very common mistake to define functions of the wrong type. You can protect yourself a little bit by loading declarations like the following after loading your solution:
<sample>+= [<-D]
(* first declaration for sanity check *)
val compound : ('a * 'a -> 'a) -> int -> 'a -> 'a = compound
val exp : int -> int -> int = exp
val fib : int -> int = fib
val firstVowel : char list -> bool = firstVowel
val null : 'a list -> bool = null
val rev : 'a list -> 'a list = rev
val minlist : int list -> int = minlist
val foldl : ('a * 'b -> 'b) -> 'b -> 'a list -> 'b = foldl
val foldr : ('a * 'b -> 'b) -> 'b -> 'a list -> 'b = foldr
val zip : 'a list * 'b list -> ('a * 'b) list = zip
val pairfoldr : ('a * 'b * 'c -> 'c) -> 'c -> 'a list * 'b list -> 'c = pairfoldr
val unzip : ('a * 'b) list -> 'a list * 'b list = unzip
val flatten : 'a list list -> 'a list = flatten
val nth : int -> 'a list -> 'a = nth
val emptyEnv : 'a env = emptyEnv
val bindVar : string * 'a * 'a env -> 'a env = bindVar
val lookup  : string * 'a env -> 'a = lookup
val isBound : string * 'a env -> bool = isBound
val extendEnv : string list * 'a list * 'a env -> 'a env = extendEnv
val addelt : 'a * 'a set -> 'a set = addelt
val treeFoldr : ('a * 'b -> 'b) -> 'b -> 'a tree -> 'b = treeFoldr
val setFold : ('a * 'b -> 'b) -> 'b -> 'a set -> 'b = setFold
val scons : 'a * 'a seq -> 'a seq = scons
val ssnoc : 'a * 'a seq -> 'a seq = ssnoc
val sappend : 'a seq * 'a seq -> 'a seq = sappend
val listOfSeq : 'a seq -> 'a list = listOfSeq
val seqOfList : 'a list -> 'a seq = seqOfList
val singletonOf : 'a -> 'a flist = singletonOf
val atFinger : 'a flist -> 'a = atFinger
val fingerLeft  : 'a flist -> 'a flist = fingerLeft
val fingerRight : 'a flist -> 'a flist = fingerRight
val deleteLeft  : 'a flist -> 'a flist = deleteLeft
val deleteRight : 'a flist -> 'a flist = deleteRight
val insertLeft  : 'a * 'a flist -> 'a flist = insertLeft
val insertRight : 'a * 'a flist -> 'a flist = insertRight
val ffoldl : ('a * 'b -> 'b) -> 'b -> 'a flist -> 'b = ffoldl
val ffoldr : ('a * 'b -> 'b) -> 'b -> 'a flist -> 'b = ffoldr
(* last declaration for sanity check *)
Defines addelt, atFinger, bindVar, compound, deleteLeft, deleteRight, emptyEnv, exp, extendEnv, ffoldl, ffoldr, fib, fingerLeft, fingerRight, firstVowel, flatten, foldl, foldr, insertLeft, insertRight, isBound, listOfSeq, lookup, minlist, nth, null, pairfoldr, rev, sappend, scons, seqOfList, setFold, singletonOf, ssnoc, treeFoldr, unzip, zip (links are to index).

I don't promise to have all the functions and their types here. Making sure that every function has the right type is your job, not mine.

Avoid common mistakes

Here is a list of common mistakes to avoid:

What to submit

There is no README file for this assignment.

How your work will be evaluated

The criteria are mostly the same as for the scheme and hofs assignments.

Index and cross-reference