CSC 372, Spring 2023 Assignment 8 Due: Wednesday, May 3 at 23:59:59

The Usual Stuff

Odds and Ends

There's only one ASSIGNMENT-WIDE RESTRICTION: You may not require (i.e., import) any modules.

In the examples that use rk/i, I've elided the file name that appears before XREPL's > prompt, so instead of seeing examples like

"let-list.rkt"> (let-list h t '(a b) t)
'(b)
you'll see
> (let-list h t '(a b) t)
'(b)

Another Bash script for development: rk/loop

I hesitated to mention it in the a7 write-up because it's a little bit tricky but during office hours I've shown a number of students the rk/loop script and they've had no trouble with it. It's like rk/i but it loops: whenever you hit control-D, it exits and restarts racket. It looks like this:

% rk/loop hello.rkt
Note: one-second window after ^D and Reloading... to hit ^C
Welcome to Racket v8.5 [cs].
Hello! (v1)
"hello.rkt"> [edit hello.rkt, changing "v1" to "v2" and then type ^D]
Reloading hello.rkt
Welcome to Racket v8.5 [cs].
Hello! (v2)
"hello.rkt"> [edit hello.rkt, changing "v2" to "v3" and then type ^D]
Reloading hello.rkt
Welcome to Racket v8.5 [cs].
Hello! (v3)
...

Thus, the edit-run cycle is edit-^D-edit-^D-edit-^D..., much like Eclipse with edit-F11-edit-F11-edit-F11...

The tricky part with rk/loop is that if you want to get out of it, perhaps because you're ready to run the Tester, you've got a one-second window to hit ^C (killing rk/loop) after you hit ^D and it prints Reloading....

If you miss that one-second window, you can simply try again (and again). If you just can't seem to get out, you can always do ^Z to suspend rk/loop as a last resort. You'll see Stopped... but that's a misnomer—the "job" is only suspended. Use kill -9 %% to kill it:

% rk/loop hello.rkt
...
"hello.rkt">
Reloading hello.rkt
^Z
[1]+  Stopped                 rk/loop hello.rkt
% kill -9 %%
[1]+  Killed                  rk/loop hello.rkt
%

Problem 1. (7 points) uniq.rkt

Note: Don't overlook the restriction below!

With the -c option, the UNIX utility uniq counts consecutive occurrences of lines:

% cat uniq.1
apple
banana
banana
apple
apple
apple
carrot
carrot
% uniq -c < uniq.1
      1 apple
      2 banana
      3 apple
      2 carrot

% racket uniq.rkt < uniq.1
1 apple
2 banana
3 apple
2 carrot

If the input is empty, uniq.rkt produces no output.

% racket uniq.rkt < /dev/null
%

As you may have learned in 352, if uniq -c is given sorted input, it tabulates, and thus so does uniq.rkt:

% echo to be or not to be | fold -1 | sort | racket uniq.rkt
5
2 b
2 e
1 n
4 o
1 r
3 t

There's an interesting story involving Donald Knuth, Doug McIlroy, and uniq -c. Take a look at matt-rickard.com/instinct-and-culture for a short version of it.

RESTRICTION: Your solution must be based on folding. You may not write any recursive code, use named let, or use any sort of looping special form like any of the many variants of fors, or do.

Just like wc.rkt on slide 97 and minmax.rkt on assignment 7, uniq.rkt reads from standard input. My solution uses (port->lines) to read all the lines on standard input and return a list of them, just like sys.stdin.readlines() in Python.

Problem 2. (21 points) showfacts.rkt

The problem morefacts.txt on assignment 2 asked you and your classmates to write some facts/opinions about three languages of their choice. A compilation was posted in spring23/morefacts-s23.txt.

For this problem you are to write a Racket program that reads a file with a compilation of language facts and lets the user make queries. Running showfacts.rkt causes it to load a8/all.sf by default. Once loaded, the user can run the procedure langs to show the names of all the languages:

% rk/i showfacts.rkt
Welcome to Racket v8.5 [cs].
> (langs)
Languages:
ABC, ACcent, APL, Action!, Apache Pig Latin, AppleScript, Arduino,
Assembly, AutoHotkey, Autocode, B, Babbage, Ballerina, Bash,
...lots more...
VimScript, Visual Basic, Whitespace, Wolfram Mathematica, Zig

The procedure facts prints the facts/opinions about a specified language, preceded by the cited year(s) of origin. The language can be specified with a string or a symbol and is case-insensitive. Facts are shown in lexicographic order.

> (facts 'eiffel)
=== Eiffel ===
Year(s): 1985, 1986
----------
An object-oriented programming langauge. Many of the concpets in
Java and C# were initially introduced in Eiffel. -- anonymous
----------
The goal of Eiffel was to increase the reliability of
commercial software development. -- Anonymous

> (facts "MIRANDA")
=== Miranda ===
Year(s): 1982
----------
Functional programming lang that influenced Haskell --anonymous

> (facts 'mooranda) ; perhaps a version of Miranda for cows...
mooranda: not found

A different collection of facts can be loaded with the load procedure. a8/5.sf contains only five entries:

> (load "a8/5.sf")
> (langs)
Languages:
Elixir, Futhark, J, Nim, SQL

> (facts 'futhark)
=== Futhark ===
Year(s): 2014
----------
A language in the ML family that was partially derived from
Haskell. -- anonymous

If called with no arguments, facts prints the facts for all the languages. Given that the (load "a8/5.sf") above is most recent, there are only five to show:

> (facts)
All Facts:
=== Elixir ===
Year(s): 2012
----------
A functional language based on the Erlang VM intended to create
distributed and fault-tolerant systems. -- Anonymous

=== Futhark ===
Year(s): 2014
----------
A language in the ML family that was partially derived from
Haskell. -- anonymous

=== J ===
Year(s): 1996
----------
It is a high-level, mathematical programming language good
for creating algorithms and analyzing problems. It is written
in portable C. -- anonymous

=== Nim ===
Year(s): 2008
----------
general-purpose, multi-paradigm, statically typed, compiled
systems programming language. Can compile everything from C
to JavaScript. Illustrates you don't need to sacrifice performance
for expressiveness

=== SQL ===
Year(s): 1970
----------
it is used for managing relational databases and performing
action on the data. -- Surinder Singh

Implementation notes for showfacts.rkt

Here's a very simple file of facts:
% cat a8/simple.sf
Y 2011: Something about Y.
X 2005: A poorly named language.
Y 2012: Y is statically typed.
Each line in all of the input files has a language name followed by a four-digit year of origin, immediately followed by a colon and then some sort of fact or opinion about the language.

Assume that all lines in all input files are well-formed. You won't see lines with two-digit dates or missing dates. All lines have exactly one colon. (Hint: since there's just one colon, you can use string-split to separate the name and year from the blurb.)

The big, all-inclusive input file is a8/all.sf. Browse through it.

Along with simple.sf shown above, the a8 directory also has 5.sf and 10.sf, which are 5- and 10-line collections of facts/opinions.

As mentioned above, running showfacts.rkt causes it to load a8/all.sf by default. Do that by having

(load "a8/all.sf")
at the end of your showfacts.rkt.

Let's look at the central data structure, an association list named langs-alist, that my solution creates for a8/simple.sf:

> (load "a8/simple.sf")

> langs-alist
'(("X" . #((2005) ("A poorly named language.")))
  ("Y" . #((2012 2011) ("Y is statically typed." "Something about Y."))))
  
> (length langs-alist)
2

We see that langs-alist has two dotted-pairs, one for X and one for Y. Let's do a case-insensitive lookup for Y:

> (assoc "y" langs-alist string-ci=?)
'("Y" . #((2012 2011) ("Y is statically typed." "Something about Y.")))
The cdr of that dotted-pair for Y, which assoc returned, is a vector with two elements. Let's bind v to that vector and then explore v:
> (define v (cdr (assoc "y" langs-alist string-ci=?)))
> v
'#((2012 2011) ("Y is statically typed." "Something about Y."))

> (vector-length v)
2

> (vector-ref v 0)
'(2012 2011)

> (vector-ref v 1)
'("Y is statically typed." "Something about Y.")
The first element of v is a list of years. Let's add the year 2010 to it:
> (vector-set! v 0 (cons 2010 (vector-ref v 0)))
We see that change reflected in v and, most importantly, langs-alist:
> v
'#((2010 2012 2011) ("Y is statically typed." "Something about Y."))

> langs-alist
'(("X" . #((2005) ("A poorly named language.")))
  ("Y" . #((2010 2012 2011) ("Y is statically typed." "Something about Y."))))

The process above, of finding an association list entry for a language and adding entries to the list is perhaps the crux of this problem. You'll also need to think about the case of creating the first association list entry for a language.

A miserable headache related to the example above

Above I started with (load "a8/simple.sf") and then worked with langs-alist, the association list my code created from a8/simple.sf. However, the example doesn't work if I create langs-alist with a literal! Observe and weep:
> (define langs-alist '(("X" . #((2005) ("A poorly named language.")))
   ("Y" . #((2012 2011) ("Y is statically typed." "Something about Y.")))))

> (define v (cdr (assoc "y" langs-alist string-ci=?)))

> (vector-set! v 0 (cons 2010 (vector-ref v 0)))
vector-set!: contract violation
  expected: (and/c vector? (not/c immutable?))
  given: '#((2012 2011) ("Y is statically typed." "Something about Y."))

The problem is that the vector literal syntax, #(expr expr expr ...) creates an immutable vector! Let's confirm that:

> (immutable? v)
#t
At this point I'm starting to lose the will to live but...if I put that S-expression into a file and then (read ...) it, I get mutable vectors. Observe:
% cat a8/alist.txt
(("X" . #((2005) ("A poorly named language.")))
 ("Y" . #((2012 2011) ("Y is statically typed." "Something about Y."))))

% racket
> (define a (read (open-input-file "a8/alist.txt")))
> (define v (cdr (assoc "y" a string-ci=?)))
> (vector-set! v 0 (cons 2010 (vector-ref v 0)))
> a
'(("X" . #((2005) ("A poorly named language.")))
  ("Y" . #((2010 2012 2011) ("Y is statically typed." "Something about Y."))))

There's a long story behind this behavior but in short, it rises from differing "reader" behaviors when reading code vs. data. I'll say that the Racket design decision of having #(...) create an immutable vector has produced a far-flung problem: an instructor teaching Racket (me!) has had to say quite a bit about this behavior. He wonders how many students have found the will to hang on and read this far!

In Chez Scheme, #(...) creates a mutable vector, although it does need to be quoted:

> (define v '#(1 2))
> (vector-set! v 0 'x)
> v
#(x 2)

Once again I find myself wondering if maybe I should have taught Scheme using Chez Scheme instead of teaching Racket, although Racket certainly seems to be the high ground in the Scheme community.

Problem 3. (28 points) optab.rkt

One way to learn about a language's types and operators is to manually create tables that show what type results from applying a binary operator to various pairs of types. For this problem you are to write a Racket program, optab.rkt, that generates such tables for Java, Python, and Haskell.

Here's a run of optab using rk/i:

% rk/i optab.rkt
Welcome to Racket v8.5 [cs].
> (optab python * ISL)
 * | I  S  L
---+---------
 I | I  S  L
 S | S  *  *
 L | L  *  *
Here's what optab's three arguments mean:

optab's output is a table showing the type that results from applying the * operator to various pairs of types in Python. The row headings on the left specify the type of the left-hand operand. The column headings along the top specify the type of the right-hand operand.

Here are some notes on interpreting the table shown above:

Here's an example with Java:

> (optab java * IFDCS)
 * | I  F  D  C  S
---+---------------
 I | I  F  D  I  *
 F | F  F  D  F  *
 D | D  D  D  D  *
 C | I  F  D  I  *
 S | *  *  *  *  *
I, F, D, C, and S stand for int, float, double, char, and String, respectively.

Here's how optab is intended to work:

For the specified operator and types, try each pairwise combination of types with the given operator by executing that expression in the specified language and seeing what type is produced, or if an error is produced. Present the results in a table.

The table just above was produced by generating and then running each of twenty-five different Java programs and analyzing their output. Here's what the first Java program looked like:

% cat tryop.java
public class tryop {
   public static void main(String args[]) {
     printClass(1 * 1);
   }
   private static void printClass(Object o) {
      System.out.println(o.getClass().getName());
   }
}
Note the third line, f(1 * 1); That's an int times an int because the first operation to test is I * I.

The program relies on Java's "boxing" mechanism to convert the result of 1 * 1 into an object whose type can be queried with getClass(). You've seen this before, on Haskell slide 74.

Remember: A Racket program wrote that Java program, tryop.java

Let's run it by hand:

% java tryop.java
java.lang.Integer
%

That single line of output, java.lang.Integer, tells us that in Java, multiplying an int by an int produces an int, which is "boxed" into an instance of Integer.

Here's the tryop.java that's generated for I * S:

% cat tryop.java
public class tryop {
   public static void main(String args[]) {
     printClass(1 * "abc");
   }
   private static void printClass(Object o) {
      System.out.println(o.getClass().getName());
   }
}
Note that it is identical to the tryop.java generated for I * I with one exception: the third line is different: instead of being 1 * 1 it's 1 * "abc".

Let's run it:

% java tryop.java
tryop.java:3: error: bad operand types for binary operator '*'
     printClass(1 * "abc");
                  ^
  first type:  int
  second type: String
1 error
error: compilation failed

The compilation error tells us that in Java, it's not valid to multiply an int by a String.

We need a way in Racket to run a command like java tryop.java and capture its output. The procedure run-command, in a8/optab-starter.rkt does that. Let's try it, first with the tryop.java that was generated for I * I:

% cat tryop.java
public class tryop {
   public static void main(String args[]) {
     printClass(1 * 1);
   }
   private static void printClass(Object o) {
      System.out.println(o.getClass().getName());
   }
}

% rk/i a8/optab-starter.rkt
Welcome to Racket v8.5 [cs].
> (run-command "/usr/bin/java" "tryop.java")
'("java.lang.Integer\n" . "")
>

Note that run-command was called with two arguments: (1) The full path to the java "executable", /usr/bin/java, and (2) the name of the Java source file to run, tryop.java.

run-command returns a dotted-pair. The car is the data that was written to "standard output" when /usr/bin/java" tryop.java was run. The cdr is the data that was written to "error output". We see that "java.lang.Integer\n" was written to standard output, and that nothing was written to error output—the cdr is an empty string.

Let's try it with the tryop.java generated for I * S:

% cat tryop.java
public class tryop {
   public static void main(String args[]) {
     printClass(1 * "abc");
   }
   private static void printClass(Object o) {
      System.out.println(o.getClass().getName());
   }
}

% rk/i a8/optab-starter.rkt
Welcome to Racket v8.5 [cs].
> (run-command "/usr/bin/java" "tryop.java")
'(""
  .
  "tryop.java:3: error: bad operand types for binary operator '*'\n
  printClass(1 * \"abc\");\n                  ^\n  first type:  int\n
  second type: String\n1 error\nerror: compilation failed\n")

In this case, with an invalid combination of operands for * in Java, there's no standard output but the error output contains a multi-line message.

It appears that with Java, we can distinguish between valid cases (like I * I) and invalid cases (like I * S) based on whether data was written to "standard output" or "error output". Then, we can translate that success or failure into either a table entry like "I" for an int result, or "*"for an error.

Let's try Haskell with the / operator. "D" is for Double.

> (optab haskell / IDS)
 / | I  D  S
---+---------
 I | *  *  *
 D | *  D  *
 S | *  *  *
For the first case, I / I, Racket generated this file, tryop.hs:
% cat tryop.hs
(1::Integer) / (1::Integer)
:type it
Note that just a plain 1 was good enough for Java since the literal 1 has the type int but with Haskell we use (1::Integer) to be sure the type is Integer. (Yes; Integer, not Int.)

Let's try running it. For Java we used (run-command "/usr/bin/java" "tryop.java") but for Haskell we need something more elaborate:

> (run-command "/bin/bash" "-c" "ghci -ignore-dot-ghci < tryop.hs")
'("GHCi, version 8.6.5: http://www.haskell.org/ghc/  :? for help\n
Prelude> Prelude> Prelude> Prelude> Leaving GHCi.\n"
  .
  "\n:1:1: error:\n    • No instance for (Fractional Integer)
  arising from a use of ‘/’\n    • In the expression: (1 :: Integer)
  / (1 :: Integer)\n...lots more...")
Ouch—an error! That's going to be a "*". Here's the tryop.hs file generated for D * D:
% cat tryop.hs
(1.0::Double) * (1.0::Double)
:type it
Let's try it:
> (run-command "/bin/bash" "-c" "ghci -ignore-dot-ghci < tryop.hs")
'("GHCi, version 8.6.5: http://www.haskell.org/ghc/  :? for help\nPrelude>
Prelude> 1.0\nPrelude> it :: Double\nPrelude> Leaving GHCi.\n"
  .
  "")
If we look closely at the output above, we see it, with a type: it :: Double (underlined above for emphasis).

Thus, for the case I / I we need the table entry to be "*" but for D / D we want "D".

In pseudo-code, here's what optab needs to do:

For each pairwise combination of types specified in the call to optab...

  1. Generate a source code file in the specified language to test the combination at hand.

  2. Run the file using the run-command procedure. (Copy it from a8/optab-starter.rkt into your optab.rkt.)

  3. Analyze the dotted-pair produced by run-command, determining either the type produced or that an error was produced.

  4. Add an appropriate entry for the combination to the table—either a single letter for the type or an asterisk to indicate an error.

The examples above show Java and Haskell testing programs and their execution. You'll need to figure out how to do the same for Python, but let us know if you have trouble with that. The Bash command type python3 will show you where the python3 executable resides. (Do help type to learn about the type command, which is a Bash built-in.)

I chose the names tryop.java and tryop.hs for the generated files but you can use any names you want.

Below is a listing of a8/mkfile.rkt. It is a complete program that generates a file named hello.java and runs it.

% cat a8/mkfile.rkt
#lang racket

;
; `template` has the code for a Java class named hello.  Each line
; of code is represented by a string, and those strings are in
; a list that is then joined into one string, with newlines between
; lines.
;
; Note that the println has two "~a" escapes.  Further below we
; use `format` to interpolate a couple of values into the template
; (and that's why we call it `template`).
;
; Try printing it with (println template).
;
(define template
    (string-join
        '(
            "public class hello {"
            "   public static void main(String args[]) {"
            "       System.out.println(~a * ~a);"
            "    }"
            "}\n"
         ) "\n"))

;
; Open a file named "hello.java" for output, replacing it
; if it already exists.  Assign the resulting "port" to p.
;
(define p (open-output-file "hello.java" #:exists 'replace))

;
; Use "format" to interpolate 1.2 and 'x' into `template`, putting
; the resulting into `program`.
;
; Try (println program) to see what gets built.  Also try
; the commented define and see what it produces.
;
(define program (format template 1.2 "'x'"))
;(define program (format template 3 "\"testing\""))

;
; Write the program to the output file and close it.  write-string
; returns the number of characters written to the file, and because
; this call is a top-level expression, the number of characters
; written (111) appears on standard output.
(write-string program p)

(close-output-port p)

;
; For simplicity, here's another copy of run-command.
(define (run-command . args)
    (define-values (sp out in err)
        (apply subprocess #f #f #f args))
    (let ([stdout (port->string out)]
          [stderr (port->string err)])
        (close-input-port out)
        (close-output-port in)
        (close-input-port err)
        (subprocess-wait sp)
        (cons stdout stderr)))

;
; Run the just-created hello.java
(run-command "/usr/bin/java" "hello.java")
% racket a8/mkfile.rkt
111 (number of characters written by write-string--see code above)
'("144.0\n" . "")
% ls -l hello.java
-rw-rw-r-- 1 whm whm 111 Apr 20 22:32 hello.java
Here's the file that was created:
% cat hello.java
public class hello {
   public static void main(String args[]) {
       System.out.println(1.2 * 'x');
    }
}
Copy a8/mkfile.rkt into your a8 directory on lectura and run it, to help you get the idea of generating a program, running it, and then doing something with its output.

The following table shows what types must be supported in each language, and a good expression to use for testing with that type.

Letter Haskell Java Python
I (1::Integer) 1 1
F (1.0::Float) 1.0F 1.0
D (1.0::Double) 1.0 not supported
B True true True
C 'c' 'c' not supported
S "abc" "abc" "abc"
O not supported new Object() not supported
L not supported not supported [1]

Implementation notes for optab.rkt

Problem 4. (3 points) let-list.rkt

Write a macro let-list that behaves like this:

> (let-list h t '(a b c) (printf "h: ~a, t: ~a\n" h t))
h: a, t: (b c)

> (let-list x xs (range 1 5) (displayln x) (displayln xs))
1
(2 3 4)

> (let-list c cs (string->list "abcd") (list->string (list* c c cs)))
"aabcd"
Here's a description: The bindings established by let-list are only visible in the enclosed expressions:
> (let-list x xs (range 1 5) (displayln x) (displayln xs))
1
(2 3 4)
> x
x: undefined;
...
> xs
xs: undefined;
...

> (define x 5)
> (let-list x y (cons 3 4) (cons y x))
'(4 . 3)
> x
5

Problem 5. (5 points) j-for.rkt

We've had another letter from Cuthbert at Camp Racket. He found our += macro to be just what he wanted! Now he's wondering if we can recreate something like Java's for loop in Racket, so let's do that.

Here's a Java for-loop:

for (int i = 1; i <= 5; i += 1)
{
    System.out.printf("i = %d\n", i);
}
Here's the corresponding j-for, assuming that we've put a copy of our += macro in j-for.rkt:
> (j-for (i 1) (<= i 5) (+= i 1)
       (printf "i = ~a\n" i))
i = 1
i = 2
i = 3
i = 4
i = 5
> (j-for (L '(a b c)) (pair? L) (set! L (cdr L))
        (displayln (car L))
        (displayln (cdr L))
        (displayln "------"))
a
(b c)
------
b
(c)
------
c
()
------

> (j-for (vals (map odd? '(3 1 5 2 9))) (car vals) empty
          (displayln "odd")
          (set! vals (cdr vals)))
odd
odd
odd

As a simplification, j-for's first argument must have the form (identifier expr). We can't make an infinite loop like this,
(j-for empty (displayln "loop") empty)
but we could do this,
(j-for (x 0) (displayln "loop") empty)
or this,
(j-for (x 0) 0 (displayln "loop"))

In general, here's the syntax of j-for:

(j-for (var init-expr ) test-expr advance-expr
     body-form1
     ...
     body-formN)

As the infinite loop examples demonstrate, there may be zero body-forms.

Problem 6. (6 points) double.rkt

Write a macro double that "doubles" the value of each of the variables it is given as parameters:

> (define x 3)
> (define y 7)
> (double x y)
> x
6
> y
14
The same variable may be specified multiple times:
> (define i 1)
> (double i i i i i)
> i
32

> (double i)
> i
64

> (let ([a 1/1024]) (double a a a a a a) a)
1/16

Along with doubling a number, double also "doubles" strings, lists, and vectors:

> s
"testing"

> lst
'(a b c d)

> v
'#(10 20 30)

> (double s lst v)
> s
"testingtesting"
> lst
'(a b c d a b c d)
> v
'#(10 20 30 10 20 30)

> (define s "|")
> (double s s s s s s)
> s
"||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||"
Let's combine double with our j-for:
> (j-for (x 1/3) (< x 3) (double x)
               (displayln x))
1/3
2/3
4/3
8/3

Along with performing the side effects of "doubling" the given variables, double returns #<void>.

> (println (double x y))
#<void>

If double is given no arguments, it has no effect.

> (double)
>

The behavior of double is undefined if called with anything other than variables having numbers, strings, lists, or vectors for values.

Problem 7. Extra Credit optab-extra.txt

For three points of extra credit per language, have your optab.rkt support up to three additional languages of your choice. PHP, Ruby, and Standard ML come to mind as easy possibilities since they're all installed on lectura. but it's also fine to support a language that's only installed on your machine.

Details:

The burden of proof for this extra credit is on you, not me!

Problem 8. Extra Credit macro-extra.txt

For up to three points of extra credit, devise and implement a macro of your own design. Something as simple as let-list would be worth a point. Something as complicated as double would likely be worth three points.

Be sure that your creation is something that needs to be a macro. For example, something like (add 3 4) that produces 7 could be implemented as a macro but an ordinary procedure would suffice. In contrast, there's no way to implement things like +=, show, and while in the slides without using a macro—they're special forms.

Submit a plain text file macro-extra.txt, that shows your macro in action with at least two different examples.

The burden of proof for this extra credit is on you, not me!

Problem 9. Extra Credit observations.txt

Submit a plain text file named observations.txt with...

(a) (1 point extra credit) An estimate of how many hours it took you to complete this assignment. Put that estimate on a line by itself, like this:

Hours: 9.5
There should be only one "Hours:" line in observations.txt. (It's fine if you care to provide per-problem times, and that data is useful to us, but report it in some form of your own invention that doesn't contain the string "Hours:". Thanks!)

Feedback and comments about the assignment are welcome, too. Was it too long, too hard, too detailed? Speak up! I appreciate all feedback, favorable or not.

(b) (1-3 points extra credit) Cite an interesting course-related observation (or observations) that you made while working on the assignment. The observation should have at least a little bit of depth. Think of me thinking "Good!" as one point, "Excellent!" as two points, and "Wow!" as three points. I'm looking for quality, not quantity.

Turning in your work

Use a8/turnin to submit your work.

My solutions

Here's what I see for my solutions at the moment, using a8/rksize, which counts the number of left parentheses, square brackets, and curly braces— which I claim is a pretty good proxy for the "size" of a body of Racket code.

% a8/rksize $(grep -v txt a8/delivs)
uniq.rkt: 31
showfacts.rkt: 132
optab.rkt: 217
let-list.rkt: 11
j-for.rkt: 19
double.rkt: 27

Miscellaneous

Point values of problems correspond closely to the "assignment points" mentioned in the syllabus. For example, a 10-point problem would correspond to about 1% of your final grade in the course.

Feel free to use comments as you see fit, but no comments are required in your code.

Remember that late assignments are not accepted and that there are no late days; but if circumstances beyond your control interfere with your work on this assignment, there may be grounds for an extension. See the syllabus for details.

My estimate is that it will take a typical CS junior from 8 to 10 hours to complete this assignment, assuming they successfully completed assignment 7.

Our goal is that everybody gets 100% on this assignment AND gets it done in an amount of time that is reasonable for them.

Assignments are not take-home tests! We hope you'll make use of Piazza, email, Discord and office hours if problems arise. If you're getting toward the hours I estimate above and don't seem to be close to completing it, or you're simply worried about your progress, email us! Give us a chance to speed you up!