OPTIONAL Solo Assignment F
-
Dueish: Fri. Dec. 18. (But keep in mind that all work will be accepted through Sat. Jan 02.)
- Notes:
- This solo assignment is completely optional.
- This is a solo assignment. You must complete all parts of this assignment entirely on your own without help from any other person and without consulting any resources other than course materials or SML documentation. You may ask Lyn for clarification, but not for help.
- You can do all problems on this assignment based on material you learned from PS5 and PS6. Be sure to study the solutions to PS5 and PS6 as part of doing this assignment.
- This assignment has one solo problem worth 33 points.
- This is the only solo problem that is worth extra credit points. Any points you earn on this assignment will count as extra points toward the solo component of your grade but will not count toward the 50 point extra credit limit on extra credit points mentioned in the syllabus.
- Of course, partial credit will be awarded where appropriate for incomplete problems.
-
How to Start Solo Assignment F
-
Follow the instructions in the GDoc CS251-F20-T2 Pset Submission Instructions for creating your SOLO-F Google Doc. Put all written answers and a copy of all code into this SOLO-F GDoc.
-
This problem involves starter files in the
~/cs251/sml/solo-f
directory in yourcsenv
Virtual Machine.To create this directory, execute this Linux commands in a shell on your csenv appliance:
cd ~/cs251/sml git pull origin master
If you encounter any issues when executing these commands, you can troubleshoot by following these git instructions. If these instructions don’t help, please contact Lyn.
-
Rename the starter file
MiniFP.sml
in~/cs251/sml/solo-f
toyourAccountName-MiniFP.sml
. -
Rename the starter file
MiniFPInterp.sml
in~/cs251/sml/solo-f
toyourAccountName-MiniFPInterp.sml
. -
Rename the starter file
MiniFPInterpTest.sml
in~/cs251/sml/solo-f
toyourAccountName-MiniFPInterpTest.sml
. -
In your renamed file
yourAccountName-MiniFPInterp.sml
, change the first line of the file fromuse "../solo-f/MiniFP.sml";
to
use "../solo-f/yourAccountName-MiniFP.sml";
-
In your renamed file
yourAccountName-MiniFPInterpTest.sml
, change the first line of the file fromuse "../solo-f/MiniFPInterp.sml";
to
use "../solo-f/yourAccountName-MiniFPInterp.sml";
-
REMINDER: ALWAYS MAKE A BACKUP OF YOUR .sml FILES AT THE END OF EACH DAY TO AVOID POTENTIAL LOSS OF WORK.
-
- Submission:
- Include all code that you write in
yourAccountName-MiniFP.sml
andyourAccountName-MiniFPInterp.sml
and in your SOLO-F GDoc (so that I can comment on it). In your GDoc only include the function definitions you modify plus any new functions you define. - Please format your SML function definitions so that they’re easy to read. Format all code using a fixed-width font (Courier New or Consolas). You use a small font size if that helps.
-
Drop a copies of your
yourAccountName-MiniFP.sml
andyourAccountName-MiniFPInterp.sml
files in your~/cs251/drop/solo-e
drop folder oncs.wellesley.edu
by executing the following (replacing **all three occurrences ofgdome
by your cs server username):scp -r ~/cs251/sml/solo-f/gdome-MiniFP.sml gdome@cs.wellesley.edu:/students/gdome/cs251/drop/solo-f scp -r ~/cs251/sml/solo-f/gdome-MiniFPInterp.sml gdome@cs.wellesley.edu:/students/gdome/cs251/drop/solo-f
(You do not need to drop your
yourAccountName-MiniFPInterpTest.sml
file)
- Include all code that you write in
1. MiniFP Interpreter (33 points)
This is a solo problem. This means you must complete it entirely on your own without help from any other person and without consulting resources other than course materials or online documentation. You may ask Lyn for clarification, but not for help.
Background
In Section 11 of his Turing Award lecture paper, John Backus describes an applicative language named FP, which you studied in PS5 Problem 1. (Make sure you complete that problem before you start this one!) n In this problem, you will flesh out the skeleton of an interpreter for MiniFP, a subset of FP that implements many of FP’s features. The fact that functional forms in FP (and MiniFP) have implicit parameters (rather than parameters with explicit names) significantly simplifies the structure of this interpreter. In particular, the environment structures used to handle naming in Bindex are not necessary in MiniFP.
Objects
The objects manipulated by MiniFP programs are specified by the obj
data type in the file MiniFP.sml
:
datatype obj =
Int of int (* integer objects *)
| Seq of obj list (* sequence of objects *)
MiniFP objects are either:
- integers (introduced by the
Int
constructor); or - sequences of objects (introduced by the
Seq
constructor).
(Note: Although the Int
and Seq
constructors have the same names as constructors in the Sexp
, PostFix
, Intex
, and Bindex
modules, the MiniFP constructors are different. They are defined in the MiniFP
module in the file MiniFP.sml
in the solo-f
directory. Explicit qualification can be used to disambiguate the constructors from these modules — e.g., Sexp.Int
vs. MiniFP.Int
.)
The following table shows how some objects in FP notation can be expressed as instances of the obj
data type, and also in an s-expression notation. In the table, \(i\) denotes an integer and \(x\) denotes an object.
FP Notation | SML obj Notation | S-expression Notation |
---|---|---|
\(i\) | Int \(i\) |
\(i\) |
\(\langle x_1, \dots, x_n\rangle\) | Seq[ \(x_1\), \(\ldots\), \(x_n\)] |
( \(x_1 \ldots\ x_n\)) |
\(17\) | Int 17 |
17 |
\(\langle 8, -3, 6 \rangle\) | Seq[Int 8, Int ~3, Int 6] |
(8 -3 6) |
\(\langle 17, \langle 8, -3, 6\rangle \rangle\) | Seq[Int 17, Seq[Int 8, Int ~3, Int 6]] |
(17 (8 -3 6)) |
\(\langle \langle 8, -3, 6\rangle, \langle 5, 1, 7\rangle\rangle\) | Seq[Seq[Int 8, Int ~3, Int 6], Seq[Int 5, Int 1, Int 7]] |
((8 -3 6) (5 1 7)) |
Here are some definitions of MiniFP objects in the file MiniFP.sml
that we will use in later examples. Each of these has type MiniFP.obj
.
val vector1 = Seq[Int 2, Int 3, Int 5]
val vector2 = Seq[Int 10, Int 20, Int 30]
val vectors = Seq[vector1, vector2] (* A pair of vectors or a 2x3 matrix *)
val matrix = Seq[Seq[Int 1, Int 4], Seq[Int 8, Int 6], Seq[Int 7, Int 9]] (* A 3x2 matrix *)
val matrices1 = Seq[vectors, matrix] (* A pair of a 2x3 matrix and a 3x2 matrix *)
val matrices2 = Seq[matrix, vectors] (* A pair of a 3x2 matrix and a 2x3 matrix *)
For comparison, here are the s-expression forms of the these objects, also defined in MiniFP.sml
. Each of these has type Sexp.sexp
.
val vector1_sx = Sexp.stringToSexp("(2 3 5)")
val vector2_sx = Sexp.stringToSexp("(10 20 30)")
val vectors_sx = Sexp.Seq([vector1_sx, vector2_sx])
val matrix_sx = Sexp.stringToSexp("((1 4) (8 6) (7 9))")
val matrices1_sx = Sexp.Seq([vectors_sx, matrix_sx])
val matrices2_sx = Sexp.Seq([matrix_sx, vectors_sx])
Because both MiniFP
and Sexp
have datatype constructors named Int
and Seq
, the corresponding versions of this seem similar in printed representation. For example:
- MiniFP.matrix;
val it = Seq [Seq [Int 1,Int 4],Seq [Int 8,Int 6],Seq [Int 7,Int 9]]
: MiniFP.obj
- MiniFP.matrix_sx;
val it = Seq [Seq [Int 1,Int 4],Seq [Int 8,Int 6],Seq [Int 7,Int 9]]
: Sexp.sexp
However, notice that matrix
has type MiniFP.obj
and matrix_sx
has type Sexp.sexp
. In fully qualified form, matrix
would be written:
MiniFP.Seq [MiniFP.Seq [MiniFP.Int 1, MiniFP.Int 4],
MiniFP.Seq [MiniFP.Int 8, MiniFP.Int 6],
MiniFP.Seq [MiniFP.Int 7, MiniFP.Int 9]]
while matrix_sx
would be written
Sexp.Seq [Sexp.Seq [Sexp.Int 1, Sexp.Int 4],
Sexp.Seq [Sexp.Int 8, Sexp.Int 6],
Sexp.Seq [Sexp.Int 7, Sexp.Int 9]]
MiniFP.sml
includes the following functions for converting between strings, s-expressions, and MiniFP objects:
- sexpToObj;
val it = fn : Sexp.sexp -> obj
- stringToObj;
val it = fn : string -> obj
- objToSexp;
val it = fn : obj -> Sexp.sexp
- objToString;
val it = fn : obj -> string
For example:
- matrix = sexpToObj matrix_sx;
val it = true : bool
- matrix = stringToObj "((1 4) (8 6) (7 9))";
val it = true : bool
- Sexp.isEqual(objToSexp matrix, matrix_sx);
val it = true : bool
(* Recall that Sexp.sexp is *not* an equality type, so can't compare
two sexps with =. Need to use Sexp.isEqual instead. *)
- objToString matrix;
val it = "((1 4) (8 6) (7 9))" : string
Functional Forms
MiniFP programs are functional forms which include primitive functions like \(\mathrm{id}\), \(\mathrm{+}\), and \(\mathrm{distr}\), as well as combining forms like composition (\(\circ\)), mapping (\(\alpha\)), and reduction (/).
Here is the data type funForm
for representing functional forms in SML:
and funForm =
Id (* identity *)
| Add (* addition *)
| Sub (* subtraction *)
| Mul (* multiplication *)
| Div (* division *)
| Distl (* distribute from left *)
| Distr (* distribute from right *)
| Trans (* transpose *)
| Const of obj (* constant function *)
| Sel of int (* selection *)
| Map of funForm (* alpha = map *)
| Reduce of funForm (* / = reduce *)
| BinaryToUnary of funForm * obj (* bu *)
| Compose of funForm * funForm (* o = composition *)
| FunSeq of funForm list (* [...] = function sequence *)
The following table shows the correspondence between FP notation for functional forms, the SML data type notation, and an s-expression notation. In the table, \(i\) denotes an integer, \(x\) denotes an object, and \(f\) denotes a functional form.
FP Notation | SML funForm Notation | S-expression notation |
---|---|---|
\(\mathrm{id}\) | Id |
id |
\(+\) | Add |
+ |
\(-\) | Sub |
- |
\(\times\) | Mul |
x |
\(\div\) | Div |
-: |
\(\mathrm{distl}\) | Distl |
distl |
\(\mathrm{distr}\) | Distr |
distr |
\(\mathrm{trans}\) | Trans |
trans |
\(\overline{x}\) | Const \(x\) |
($ \(\ x\)) |
\(i\) | Sel \(i\) |
\(i\) |
\(\alpha\,f\) | Map \(f\) |
(a \(\ f\)) |
\(/\,f\) | Reduce \(f\) |
(/ \(\,f\)) |
\(\mathrm{bu}\ \,f\ x\) | BinaryToUnary( \(f\), \(x\)) |
(bu \(\ f\) \(x\)) |
\(f_1 \circ\ f_2\) | Compose( \(f_1\), \(f_2\)) |
(o \(\ f_1\) \(f_2\)) |
\([f_1 \dots\ f_n]\) | FunSeq[ \(f_1\), \(\dots\), \(f_n\)] |
( \(f_1\ \dots\ f_n\)) |
For example, consider the inner product and matrix multiplication examples from Sections 11.3.2 and 11.3.3 (p. 622) of Backus’s paper:
\[
\begin{array}{rcl}
IP & \equiv{} & (/+)\circ(\alpha{}\times)\circ\mathrm{trans} \
MM & \equiv{} & (\alpha\alpha\mathrm{IP})\circ(\alpha\mathrm{distl})\circ\mathrm{distr}\circ[1,\mathrm{trans}\circ2]
\end{array}
\]
These can be expressed as SML MiniFP.funForm
instances via the following definitions (included in MiniFP.sml
):
val IP = Compose(Reduce Add, Compose(Map Mul, Trans))
val MM = Compose(Map(Map IP),
Compose(Map Distl,
Compose(Distr,
FunSeq[Sel 1, Compose(Trans, Sel 2)])))
These can also be expressed as SML Sexp.sexp
instances via the following definitions (also included in MiniFP.sml
):
val IP_sx = Sexp.stringToSexp "(o (/ +) (o (a x) trans))"
val MM_sx = Sexp.stringToSexp "(o (a (a (o (/ +) (o (a x) trans))))\
\ (o (a distl) \
\ (o distr \
\ (1 (o trans 2)))))"
(The MM_sx
definition illustrates the SML convention for entering multiline strings. Each nonterminal line must end with a backslash character, and the next line must begin with a backslash character.)
MiniFP.sml
includes the following functions for converting between strings, s-expressions, and MiniFP functional forms:
- sexpToFunForm;
val it = fn : Sexp.sexp -> funForm
- stringToFunForm;
val it = fn : string -> funForm
- funFormToSexp;
val it = fn : funForm -> Sexp.sexp
- funFormToString;
val it = fn : funForm -> string
For example:
- IP = sexpToFunForm IP_sx;
val it = true : bool
- IP = stringToFunForm "(o (/ +) (o (a x) trans))";
val it = true : bool
- Sexp.isEqual(funFormToSexp IP, IP_sx);
val it = true : bool
- funFormToString IP;
val it = "(o (/ +) (o (a x) trans))" : string
1a. A MiniFP Program (5 points)
In PS5 Problem 1h, you studied this functional form:
\[ F \equiv{} (\alpha{}/+) \circ (\alpha\alpha{}\times) \circ\ (\alpha\mathrm{distl}) \circ \mathrm{distr} \circ [\mathrm{id},\mathrm{id}] \]
In the file yourAccountName-MiniFP.sml
, express this functional form in SML via two definitions that flesh out the skeleton:
val F = ...
val F_sx = ...
The value of F
should have type funForm
and the value of F_sx
should have type Sexp.sexp
.
Notes:
-
You can safely ignore this warning when when send your
yourAccountName-MiniFP.sml
buffer into the*sml*
interpreter buffer:../utils/Utils.sml:35.48 Warning: calling polyEqual
-
After you send your
yourAccountName-MiniFP.sml
buffer into the*sml*
interpreter buffer, the following equalities should hold with your definitions:- open MiniFP; ... many lines of printout omitted ... - F = sexpToFunForm F_sx; val it = true : bool - Sexp.isEqual(funFormToSexp F, F_sx); val it = true : bool
1b. A MiniFP Interpreter (28 points)
A MiniFP interpreter specifies the object that results from applying a program (i.e., functional form) to an object. So a MiniFP interpreter is simply an apply
function with the following type:
val apply: MiniFP.funForm -> MiniFP.obj -> MiniFP.obj
The semantics (meaning) of all MiniFP functional forms is given in Sections 11.2.3 and 11.2.4 of Backus’s paper. The only difference between MiniFP and FP semantics is that in MiniFP, the Reduce
functional form is defined only on nonempty sequences. So applying Reduce Add
to Seq[]
is an error in MiniFP, but not in FP (where it denotes 0).
In this task, your goal is to implement a complete MiniFP interpreter by fleshing out the skeleton of the apply
function in the file yourAccountName-MiniFPInterp.sml
. In this sekelton, the clauses for Id
, Add
, and Trans
have been written for you, and all other clauses are stubs that act like Id
. Your task is to flesh out the clauses for all the other MiniFP functional forms so that they work as expected. x
Notes:
-
Each functional form takes a
MiniFP.obj
as an input and returns aMiniFP.obj
as an output. The expected type, size (for sequences), etc. of the object depends on the details of the functional form. E.g.,distl
expects the object to be a sequence of exactly two elements, where the first element can be any object but the second element must be a sequence (with any number of elements). These constraints can be concisely specified via pattern matching in anapply
clause; for example:apply Distl (Seq[y,Seq zs]) = ...
When implementing a clause, think carefully about the constraints and be sure not to overconstrain the object. E.g., in the
Distl
clause, the first sequence value can be any objecty
; you should not force it to be a sequence valueSeq ys
, which would preclude it from being an integer. -
You can test your interpreter by sending your
yourAccountName-MiniFPInterp.sml
to the*sml*
buffer. After youopen MiniFPInterp;
, you can interactively test yourapply
function on the kinds of examples shown below. See further down for how to run a suite of test cases on your interpreter. -
You can safely ignore this warning when when send your
yourAccountName-MiniFPInterp.sml
buffer into the*sml*
interpreter buffer:../utils/Utils.sml:35.48 Warning: calling polyEqual
-
It is strongly recommended that you flesh out
apply
incrementally. That is, flesh out one clause and a time and test that before going to the next clause. -
Feel free to define and use helper functions.
-
You should raise an
EvalError
with an (usingraise EvalError
error-message-string-goes-here) when an error situation is encountered. The skeleton intepreter already raises two such errors: (1)Error: Ill-formed application
for an ill-formed application andError: transpose -- sequences not all of same length
fortranspose
. Your intepreter clauses should raise anEvalError
in the following three cases:- division by zero
- index out of bounds for
Sel
- reduction on an empty list
Here are some sample error messages show using the
testApply
function described later:- open MiniFPInterp; ... many lines of printout omitted ... - testApply Div (Seq[Int 5, Int 0]); val it = "EvalError: Division by zero" : string - testApply (Sel 4) vector1; val it = "EvalError: Selection index 4 out of bounds [1..3]" : string - testApply (Reduce Add) (Seq[]); val it = "EvalError: Reduction of empty sequence" : string
-
Below are numerous examples of the correct behavior of MiniFP’s
apply
function. Yourapply
function should behave the same way. (See further down for how to run a suite of automated test cases.)- open MiniFPInterp; ... many lines of printout omitted ... - apply; val it = fn : funForm -> obj -> obj - apply Id (Int 17); val it = Int 17 : obj - apply Add (Seq[Int 17, Int 3]); val it = Int 20 : obj - apply Mul (Seq[Int 17, Int 3]); val it = Int 51 : obj - apply Sub (Seq[Int 17, Int 3]); val it = Int 14 : obj - apply Div (Seq[Int 17, Int 3]); val it = Int 5 : obj - vector1; val it = Seq [Int 2,Int 3,Int 5] : obj - apply Id vector1; val it = Seq [Int 2,Int 3,Int 5] : obj - apply (Const (Int 17)) vector1; val it = Int 17 : obj - apply (Sel 1) vector1; val it = Int 2 : obj - apply (Sel 2) vector1; val it = Int 3 : obj - apply (Sel 3) vector1; val it = Int 5 : obj - apply Distl (Seq[Int 7, vector1]); val it = Seq [Seq [Int #,Int #],Seq [Int #,Int #],Seq [Int #,Int #]] : obj - Control.Print.printDepth := 1000; ... many lines of printout omitted ... val it = () : unit - apply Distl (Seq[Int 7, vector1]); val it = Seq [Seq [Int 7,Int 2],Seq [Int 7,Int 3],Seq [Int 7,Int 5]] : obj - apply Distr (Seq[vector1, Int 7]); val it = Seq [Seq [Int 2,Int 7],Seq [Int 3,Int 7],Seq [Int 5,Int 7]] : obj - apply (BinaryToUnary (Add, Int 1)) (Int 17); val it = Int 18 : obj - apply (Map (Const (Int 17))) vector1; val it = Seq [Int 17,Int 17,Int 17] : obj - apply (Map (BinaryToUnary (Mul, Int 2))) vector1; val it = Seq [Int 4,Int 6,Int 10] : obj - apply (Reduce Add) vector1; val it = Int 10 : obj (* 2 + (3 + 5) *) - apply (Reduce Mul) vector1; val it = Int 30 : obj (* 2 * (3 * 5) *) - apply (Reduce Sub) vector1; val it = Int 4 : obj (* 2 - (3 - 5) *) - apply (Compose(Map (BinaryToUnary (Add, Int 1)), Map (BinaryToUnary (Mul, Int 2)))) vector1; val it = Seq [Int 5,Int 7,Int 11] : obj (* You can use stringToFunForm and stringToObj to simplify testing *) - apply (stringToFunForm "(o (a (bu + 1)) (a (bu x 2)))") (stringToObj "(2 3 5)"); val it = Seq [Int 5,Int 7,Int 11] : obj - apply (FunSeq[Const (Int 17), Id, Map (BinaryToUnary (Add, Int 1))]) vector1; val it = Seq [Int 17,Seq [Int 2,Int 3,Int 5],Seq [Int 3,Int 4,Int 6]] : obj - matrix; val it = Seq [Seq [Int 1,Int 4],Seq [Int 8,Int 6],Seq [Int 7,Int 9]] : obj - apply Trans matrix; val it = Seq [Seq [Int 1,Int 8,Int 7],Seq [Int 4,Int 6,Int 9]] : obj - apply (Compose(Trans,Trans)) matrix; val it = Seq [Seq [Int 1,Int 4],Seq [Int 8,Int 6],Seq [Int 7,Int 9]] : obj - vectors; val it = Seq [Seq [Int 2,Int 3,Int 5],Seq [Int 10,Int 20,Int 30]] : obj - apply IP vectors; val it = Int 230 : obj - matrices1; val it = Seq [Seq [Seq [Int 2,Int 3,Int 5],Seq [Int 10,Int 20,Int 30]], Seq [Seq [Int 1,Int 4],Seq [Int 8,Int 6],Seq [Int 7,Int 9]]] : obj - apply MM matrices1; val it = Seq [Seq [Int 61,Int 71],Seq [Int 380,Int 430]] : obj - matrices2; val it = Seq [Seq [Seq [Int 1,Int 4],Seq [Int 8,Int 6],Seq [Int 7,Int 9]], Seq [Seq [Int 2,Int 3,Int 5],Seq [Int 10,Int 20,Int 30]]] : obj - apply MM matrices2; val it = Seq [Seq [Int 42,Int 83,Int 125], Seq [Int 76,Int 144,Int 220], (* manually reformated to look better *) Seq [Int 104,Int 201,Int 305]] : obj
-
By default, SML does not display the message of an exception, which makes examples like the following harder to understand. For example:
- apply (Sel 4) vector1; uncaught exception EvalError raised at: /tmp/emacs-region1678BTc:17.10-21.15
The issue here is an index-out-of-bounds exception, but because SML does not display the exception message, this may not be obvious.
For better testing of cases involving exceptions,
MiniFPInterp.sml
includes the following helper function:(* testApply: funForm -> obj -> string *) and testApply funForm obj = objToString (apply funForm obj) handle (EvalError msg) => "EvalError: " ^ msg | (SyntaxError msg) => "SyntaxError: " ^ msg | other => "Exception: " ^ (exnMessage other)
Here are some examples of using
testApply
to show error messages:- testApply (Sel 4) vector1; val it = "EvalError: Selection index 4 out of bounds [1..3]" : string - testApply (Reduce Add) (Seq[]); val it = "EvalError: Reduction of empty sequence" : string - testApply Div (Seq[Int 5, Int 0]); val it = "EvalError: Division by zero" : string
-
You can test your interpreter on a suite of test cases by sending your
yourAccountName-MiniFPTest.sml
buffer to your*sml*
interpreter buffer. This will load theMiniFPTest
structure defined in this file and invokeMiniFPTest.test()
, which will test your interpreter on over 100 test cases.The test cases are defined in a variable
testSuite
, each of whose entries has the form:((
function-form-string, object-string)
, result-string)
The tester first parses function-form-string and object-string into a function form f and object x, respectively. It then applies f to x, and converts the resulting object into a string. If this string matches result-string, the test passes by displaying
OK!
. Otherwise, the test fails by displaying***TESTING MISMATCH***
and showing the difference between the expected result and the actual result.If the application of f to x raises an exception, the tester converts the exception message to an error string; this makes it possible for the tester to verify that the interpreter handles error situations correctly.
Your implementation should be able to pass all the test cases in
yourAccountName-MiniFPTest.sml
. In order to do this, you will have to make the error messages in anyEvalError
exceptions you raise match the ones in the testing suite.You are encouraged to add additional test cases to
yourAccountName-miniFPTest.sml