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.
(Required) Read Mitchell, Chapter 5.
(As necessary) Read ML references, as needed for the programming questions.
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.
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.
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
and
for any \tt s and \tt t.
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?
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:
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.
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.
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
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.)
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!
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.
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:
these expression forms must return a value in the range [-1,1] if all subexpressions have values in this range, and
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.
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";
Submit your homework via GradeScope by the beginning of class on the due date.
Submit your answers to the Gradescope assignment named, for example, "HW 1". It should:
You will be asked to resubmit homework not satisfying these requirements. Please select the pages for each question when you submit.
If this homework includes programming problems, submit your code to the Gradescope assignment named, for example, "HW 1 Programming". Also:
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.