CS334: HW 4

Table Of Contents


This homework has three types of problems:


  1. (Required) Read Mitchell, Chapter 5.

  2. (As necessary) Read ML references, as needed for the programming questions.


1. ML Map for Trees (10 pts)

Mitchell, Problem 5.4

You are not required to submit a program file for this question, but you may want to double check your answer by running your solution. If you do, be sure to include

Control.Print.printDepth:= 100;

at the top of your file, so that datatypes print completely.

2. ML Reduce for Trees (10 pts)

Mitchell, Problem 5.5

You are not required to submit a program file for this question, but you may want to double check your answer by running your solution.  

3.Currying (10 pts)

Mitchell, Problem 5.6

You are not required to submit a program file for this question, but you may want to double check your answer by running your solution.  

Note that you MUST explain why the equations hold. One way to do this is to apply both sides of each equation to the same argument(s) and describe how each side evaluates to the same term. For example, show that

\[\tt UnCurry(Curry(f)) (s,t) = f (s,t)\] and \[\tt Curry(UnCurry(g))~s~t = g~s~t\]

for any \(\tt s\) and \(\tt t\).

4. Type Inference and Bugs (10 pts)

What is the type of the following ML function?

fun append(nil, l) = l
  | append(x::l, m) = append(l,m);

Write one or two sentences to explain succinctly and informally why append has the type you give. This function is intended to append one list onto another. However, it has a bug. How might knowing the type of this function help the programmer to find the bug?


1. Random Art (50 pts)

This question and the next are programming questions — you are not required to work with a partner but are encouraged to do so. As always, please send me email if you would like me to help you find a partner.

Note: The program you will write generates output files that you will need to view. Thus, I suggest you either install sml on your own computer, or use the lab machines and employ scp to copy the files to your local computer for viewing. (More details below.)

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.


This problem brings together a number of topics we have studied, including grammars, parse trees, and evaluation. Your job is to write an ML program to construct and plot randomly generated functions. The language for the functions can be described by a simple grammar: \[e ::= x ~|~ y ~|~ \sin{\pi e} ~|~ \cos{\pi e} ~|~ (e+e)/2 ~|~ e*e\] Any expression generated by this grammar is a function over the two variables \(x\) and \(y\). Note that any function in this category produces a value between -1 and 1 whenever \(x\) and \(y\) are both in that range.

We can characterize expressions in this grammar with the following ML datatype:

datatype Expr = 
    | VarY
    | Sine     of Expr
    | Cosine   of Expr
    | Average  of Expr * Expr
    | Times    of Expr * Expr;       

Note how this definition mirrors the formal grammar given above; for instance, the constructor Sine represents the application of the sin function to an argument multiplied by \(\pi\). Interpreting abstract syntax trees is much easier than trying to interpret terms directly.

  1. Printing Expressions: The first two parts require that you edit and run only expr.sml. First, write a function

    exprToString : Expr -> string

    to generate a printable version of an expression. For example, calling exprToString on the expression


    should return a string similar to “sin(pi*x)*cos(pi*x*y)”. The exact details are left to you. (Remember that string concatenation is performed with the ^ operator.)

    Test this function on a few sample inputs before moving to the next part.

  2. Expression Evaluation: Write the function

    eval : Expr -> real*real -> real

    to evaluate the given expression at the given \((x, y)\) location. You may want to use the functions Math.cos and Math.sin, as well as the floating-point value Math.pi. (Note that an expression tree represented, e.g., as Sine(VarX) corresponds to the mathematical expression \(\sin(\pi x)\), and the eval function must be defined appropriately.)

    Test this function on a few sample inputs before moving on to the next part. Here are a few sample runs:

    - eval (Sine(Average(VarX,VarY))) (0.5,0.0);
    val it = 0.707106781187 : real
    - eval sampleExpr (0.1,0.1);
    val it = 0.569335014033 : real
  3. Driver Code: The art.sml file includes the doRandomGray and doRandomColor functions, which generate grayscale and color bitmaps respectively. These functions want to loop over all the pixels in a (by default) 501 by 501 square, which naturally would be implemented by nested for loops. In art.sml, complete the definition of the function

    for : int * int * (int -> unit) -> unit

    The argument triple contains a lower bound, an upper bound, and a function; your code should apply the given function to all integers from the lower bound to the upper bound, inclusive. If the greater bound is strictly less than the lower bound, the call to for should do nothing. Implement this function using imperative features. In other words, use a ref cell and the while construct to build the for function.

    Note: It will be useful to know that you can use the expression form (e1 ; e2) to execute expression e1, throw away its result, and then execute e2. Thus, inside an expression a semicolon acts exactly like comma in C or C++. Also, the expression “()” has type unit, and can be used when you want to “do nothing”.

    Test your code with a call like the following:

    for (2, 5, (fn x => (print ((Int.toString(x)) ^ "\n"))));

    It should print out the numbers 2,3,4, and 5.

    Now produce a grayscale picture of the expression sampleExpr. You can do this by calling the emitGrayscale function. Look at doRandomGray to see how this function is used.

    If you get an uncaught exception Chr error while producing a bitmap, that is an indication that your eval function is returning a number outside the range [-1,1].

    Note: The type assigned to your for function may be more general than the type described above. How could you force it to have the specified type, and why might it be useful to do that? (You don’t need to submit an answer to this, but it is worth understanding.)

  4. Viewing Pictures: You can view pgm files, as well as the ppm files described below, on a Mac with Preview. When using other computers, or to post them on a web, etc., you might need to first convert the file to jpeg format with the following command:

    convert art.pgm art.jpg 

    The convert utility will work for both .ppm and .pgm files. You can install convert on your own machine using the instructions here: https://imagemagick.org/script/download.php. You can also try other image viewing programs, including Gimp.

    If you have connect to our Unix machines to work, you can copy files back to your own machine with scp, as in the following, where ~/cs334/lab4/arg.ppm specifies the path and file name of the file to copy.

    scp freund@limia.cs.williams.edu:~/cs334/lab4/art.ppm .

    Let us know if you have any trouble viewing your artwork!

  5. Generating Random Expressions: Your next programming task is to complete the definition of

    build(depth, rand) : int * RandomGenerator -> Expr

    The first parameter to build is a maximum nesting depth that the resulting expression should have. A bound on the nesting depth keeps the expression to a manageable size; it’s easy to write a naive expression generator which can generate incredibly enormous expressions. When you reach the cut-off point (i.e., depth is 0), you can simply return an expression with no sub-expressions, such as VarX or VarY. If you are not yet at the cut-off point, randomly select one of the forms of Expr and recursively create its subexpressions.

    The second argument to build is a function of type . As defined at the top of art.sml, the type RandomGenerator is simply a type abbreviation for a function that takes two integers and returns an integer:

    type RandomGenerator = int * int -> int

    Call rand(l,h) to get a number in the range l to h, inclusive. Successive calls to that function will return a sequence of random values. Documentation in the code describes how to make a RandomGenerator function with makeRand. You may wish to use this function while testing your build function.

    Once you have completed build, you can generate pictures by calling the function

    doRandomGray : int * int * int -> unit 

    which, given a maximum depth and two seeds for the random number generator, generates a grayscale image for a random image in the file art.pgm. You may also run

    doRandomColor : int * int * int -> unit

    which, given a maximum expression depth and two seeds for the random number generator, creates three random functions (one each for red, green, and blue), and uses them to emit a color image art.ppm. (Note the different filename extension).

    A few notes:

    • The build function should not create values of the Expr datatype directly. Instead, use the build functions buildX, buildY, buildSine, etc. that I have provided in expr.sml. This provides a degree of modularity between the definition of the Expr datatype and the client. We will look at how to enforce this separation with the ML module system in a few more weeks.

    • A depth of 8 – 12 is reasonable to start, but experiment to see what you think is best.

    • If every sort of expression can occur with equal probability at any point, it is very likely that the random expression you get will be either VarX or VarY, or something small like Times(VarX,VarY). Since small expressions produce boring pictures, you must find some way to prevent or discourage expressions with no subexpressions from being chosen “too early”. There are many options for doing this— experiment and pick one that gives you good results.

    • The two seeds for the random number generators determine the eventual picture, but are otherwise completely arbitrary.

  6. Extensions: Extend the Expr datatype with at least three more expression forms, and add the corresponding cases to exprToString, eval, and build. The two requirements for this part are that:

    1. these expression forms must return a value in the range [-1,1] if all subexpressions have values in this range, and

    2. at least one of the extensions must be constructed out of 3 subexpressions, ie. one of the new build functions must have type Expr * Expr * Expr -> Expr.

    There are no other constraints; the new functions need not even be continuous. Be creative!

    Make sure to comment your extensions.

2. Expression Representation (10 pts)

This question explores an alternative way of representing expressions in the random art program.

Create a new file expr-func.sml which, like the file expr.sml, defines the Expr representation and basic operations on it. In this version, the definition of the type Expr should be not a datatype, but:

type Expr =  real * real -> real

That is, instead of the symbolic representation used by expr.sml, this implementation will represent each function in \(x\) and \(y\) directly as an SML function of two real arguments. Redefine the following operations on the new type:

The eval function in particular becomes much, much simpler than in expr.sml, but the exprToString function cannot be written successfully, since there is no way to convert an ML function to a string. Thus, your implementation of this function can return something like "<function>" or "unknown". To test your code, replace

use "expr.sml";

at the top of art.sml with

use "expr-func.sml";

What To Turn In


Your submitted homework should:


Your submitted programs should:

To submit your code in Gradescope, navigate to the submission page for this assignment’s programming component, and select the option to submit files. Then select and upload your source files. If you worked with a partner, only one of you should submit your code, and please indicate who your partner is when you upload your files.

Note: The shared repository you are using is either your own or your partners. The other one will be unused. There is no need to do anything to that repository. Our submission scripts will ignore unused repositories and look only at the onces with completed solutions.