CS 334: HW 4

Instructions

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 encouraged 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.

Reading

  • (Required) Read Mitchell, Chapter 5.

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

Problems

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?

Programming

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.

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 = 
    VarX
    | VarY
    | Sine     of Expr
    | Cosine   of Expr
    | Average  of Expr * Expr
    | Times    of Expr * Expr;       

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

    Times(Sine(VarX),Cosine(Times(VarX,VarY)))
    

    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.

    A Bit of Handy ML Syntax

    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].

    Thought Question

    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 our Linux computers with the eog program, or 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.

    You can also copy files from your Unix account 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:

  • exprToString

  • eval

  • buildX, buildY, buildSine, etc.

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";

Submitting Your Work

Submit your homework via GradeScope by the beginning of class on the due date.

Written Problems

Submit your answers to the Gradescope assignment named, for example, "HW 1". It should:

  • be clearly written or typed,
  • include your name and HW number at the top,
  • list any students with whom you discussed the problems, and
  • be a single PDF file, with one problem per page.

You will be asked to resubmit homework not satisfying these requirements. Please select the pages for each question when you submit.

Programming Problems

If this homework includes programming problems, submit your code to the Gradescope assignment named, for example, "HW 1 Programming". Also:

  • Be sure to upload all source files for the programming questions, and please do not change the names of the starter files.
  • If you worked with a partner, only one of each pair needs to submit the code.
  • Indicate who your partner is when you submit the code to gradescope. Specifically, after you upload your files, there will be an "Add Group Member" button on the right of the Gradescope webpage -- click that and add your partner.

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.