Assessment #2: working with the basic datatypes

In this mini-project, you will write a variety of procedures that explore various aspects of the basic datatypes you learned about this past week. For this project, you will submit one file: basic-datatypes.scm.

Some background

As our programs get more complicated, the structure of our code and good names and formatting are not enough to make our code readable and correct. We need to rely on extrinsic means to ensure these things. To ensure that our code is readable, we use documentation to capture aspects of our code that are not obvious upon inspection. To ensure that our code is correct, we use tests that codify the correctness our programs through concrete examples.

During our week on software engineering fundamentals, we'll discuss these concepts in more detail. For now, we'll employ some basic documentation and testing for our program.

Documentation

For each function that you write in this mini project, include a function comment that captures the types of the function as well as describes its output in a sentence or two. For example, here is a function comment for a function that finds the minimum of three numbers:

;;; (min x y z) -> real?
;;;   x : number?
;;;   y : number?
;;;   z : number?
;;; Returns the minimum of x, y, and z
(define min-of-three
  (lambda (x y z)
    (cond 
      [(and (<= x y) (<= x z))
       x]
      [(and (<= y x) (<= y z))
       y]
      [else
       z])))

The function comment is a stylized comment that consists of the following three components:

  • (min x y z) -> number?: the signature of the function which names its arguments and describes the output type of the function. In Racket, we express the types with the predicate functions that we use in code to test whether an expression has that type. For example, this signature says that min has three arguments, x, y, and z and that it produces a number (as tested by the number? function).
  • x : number? ...: the types of each of the parameters mentioned in the signature. Like the return type of the function, we document the types of the parameters with the predicates that we would use in code to test values of those types.
  • Returns the minimum of x, y, and z: finally, we include a brief sentence or two description of the behavior and output of the function. Here, the behavior of the function is simple, so we comparatively have little to say: the function returns the minimum of its arguments.

Tests

Up until this point, we have asked you to experiment with the functions that you write in the explorations window to check for correctness. This has the upside of being fast, but if you change your code, you need manually type in all those tests again which is tedious (which in turn makes it less likely you'll recheck the correctness of your code). A better solution is to codify your tests in your code so that you can rerun the tests at will.

During our unit on software engineering fundamentals, we'll introduce you to a library that makes test authoring and execution a breeze. For now, we'll simply have you call your functions on a variety of inputs within your file. For example, the reading on [characters and strings]({{ "/readings/strings.html" | relative_url }}) introduced a function that tests whether a value is a lowercase character:

(define lower-case-char?
  (lambda (x)
    (and (char? x)
         (char-lower-case? x))))

To test this function, we can call this function on several inputs and verify that the function behaves as expected. Note that we should choose a variety of inputs that exercise the different possibilities that the code considers, for example:

(lower-case-char? 5)    ; #f
(lower-case-char? #\a)  ; #t
(lower-case-char? #\C)  ; #f
(lower-case-char? "a")  ; #f

Setting up your file

You will have one file for this assignment, basic-datatypes.scm. Here's the start of the file.

;; CSC-151-NN (TERM)
;; Mini-Project 2: Working with the basic datatypes
;; YOUR NAME HERE
;; YYYY-MM-DD
;; ACKNOWLEDGEMENTS:
;;   ....

(import image)
(import music)

Part 1: String Utilities

As you have likely discovered by now, the built-in Scheme procedures don't always immediately do what we want. For example, although we can use a combination of integer? and string->number to determine if a string contains only digits, we would prefer not to write (integer? (string->number str)) again and again and again, particularly since we might later realize that that solution is not perfect.

When most programmers discover that they need to do the same thing again and again? They create a library of utility procedures that they plan to use in other procedures. Although you are just beginning your experience as a Racket programmer, you will still find it useful to create your own set of utilities.

For each of the following functions, do the following:

{:type="a"}

  1. Write several tests that describe how the function should work. Note that you haven't written the function yet! While this seems backwards, this test-driven design is useful in the design process to help you concretize the behavior of a function.
  2. Write documentation for the function as outlined above. Again, you haven't written the function yet! Documenting before you implement a function is another useful technique to solidify a design before you go to implementation.
  3. Finally, implement the function! In implementing your function, you will learn new things about the design, correct mistakes, etc., so you should update your tests and documentation accordingly.

In your tests, make sure to consider edge cases the exercise the "boundaries" of your code, e.g., the string is empty or the string contains an unexpected character.

  • (increment-wrap n bound) takes a non-negative integer n as input and returns n+1 except if n+1 exceeds bound (also a non-negative integer), then it returns 0 instead.
  • (slight-trim str) takes a string str as input and returns str and removes a single leading space and a single trailing space on the ends of str, if they exist.
  • (starts-with? s1 s2) takes two strings s1 and s2 as input and determines if s1 start with s2.
  • (ends-with? s1 s2) takes two strings s1 and s2 as input and determines if s1 ends with s2.

Part 2: Ehrenstein Illusions

(Credit to Marty Stepp and Stuart Reges from the University of Washington for creating the original version of this assignment!)

An Ehrenstein Illusion is an optical illusion consisting of a collection of concentric circles and a diamond contained in a box. While we write the code to create a diamond, the circles will cause the sides of the diamond to look wavy!

For this part of the mini-project, you will ultimately write the following function:

  • (ehrenstein length n box-color circ-color outline-color): creates an image that contains a single Ehrenstein illusion with side length length, n circles, with the given box-color and circ-color for the box color and circle color, respectively. outline-color determines the color of the outline of the circles and the diamond.

With these functions, you should reproduce the following images as definitions in your program.

  • ehrenstein-1: a single Ehrenstein illusion of length 200, 5 circles, a "red" box, "yellow" circles, and "black" outline.

    ehrenstein-1 image
  • ehrenstein-2: a single Ehrenstein illusion of length 100, 10 circles, an "aqua" box, "orange" circles, and "black" outline.

    ehrenstein-2 image
  • ehrenstien-3: a single Ehrenstein illusion of length 50, no circles, a "white" box and circle, and "green" outline.

    ehrenstein-3 image
  1. Critical to your decomposition of an Ehrenstein image is capturing the repetitive concentric circles. To do this, we'll incrementally develop a function (ehrenstein-circles length n) that draws only the circle outlines of one Ehrenstein image. In doing so, we'll introduce how we can use the list datatype to perform repetitive computation.

  2. First let's step away from the code for a bit and develop a formula to compute the radius of an Ehrenstein circle given its position in the image. As a starting point, let's look at ehrenstein-1 which has 5 equally spaced concentric circles in a box of length 200. Imagine assigning a number to each circle, an index, starting from 0 for the innermost circle and 4 for the outermost circle. Fill out the following table that lists each circle's index and that circle's corresponding radius.

    Circle IndexRadius
    0?
    1?
    2?
    3?
    4?

    To do this, recall that the circles are evenly spaced apart and the length of the entire image is 200. What must the distance be between each circle so that the circles are spaced evenly?

    From this table, derive a formula for the size of an Ehrenstein circle in terms of its index. To check your work, manually create the circles from ehrenstein-1 by using your computed radii combined with a call to overlay to overlay each circle. To double-check your work, perform this exercise on the circles from ehrenstein-2 (10 circles, length 100) and see if your formula works for this case, too.

    (Note: you do not need to include your table in your code anywhere! This is merely a design aid to help you in the successive parts of this homework! Additionally, make sure to comment out or remove your check code for your formula once you are confident it works.)

  3. Next, let's put your formula to use. Write a function, (circles-list length n) that creates a list of the n black, outlined concentric circles that appear in an Ehrenstein image of the given length. To do so, you will need to put the following tools together:

    • You first need to create a list containing the indices of the circles. To do so, the (range n) function will be useful. (range n) produces a list of numbers in the range 0 to n-1.

    • With a list of the indices in hand, you then need to transform each index into its corresponding Ehrenstein circle. To do so, you will need to use the (map func lst) function which takes two arguments as input:

      1. The first is a function func which takes an element of the list as input and transforms it in some way.
      2. The second is the list lst whose elements will be transformed by func.

      In other words, map takes a function that transforms a single element and uses it to transform an entire list of elements by applying the function to each element uniformally.

    I recommend before trying to write circles-list to try out examples of range and map on your own to get a sense of how they work. Importantly, keeping in mind that a lambda expression is exactly that, an expression, we can call map as follows to increment all the elements of a list:

    (map (lambda (n) (+ n 1))
         (list 1 2 3 4 5))
    

    In this example, the provided lambda takes its argument n and returns n+ 1. map will apply this function to every elements of the list containing the numbers 1 through 5. The result is the list (list 2 3 4 5 6).

    For our purposes, the lambda we provide to map ought to take an index i as input and produce a single Ehrenstein circle as output according to the formula you derived in the previous part.

  4. At this point, we have a way to generate a list of circles of the correct size. We must now combine them using one of the appropriate image functions that combines images together into a single image. However, these functions, e.g., beside, expect the images to come in as individual arguments instead of a single argument that is a list. The function (apply func args) helps us use a list of values as the input to a function that takes multiple arguments. To use apply, we simply pass in the func that takes multiple arguments and the list of arguments args. For example, the call (apply beside (list circ1 circ2 circ3)) has the same effect as calling (beside circ1 circ2 circ3) for the images bound to circ1 through circ3. But now, we can call beside and other functions like with it with a list!

    Use apply along with circles-list to finally implement (ehrenstein-circles length n)!

  5. Finally, use ehrenstein-circles to complete the implementation of ehrenstein as described at the beginning of this part!

In addition to writing these functions, you should:

  • Demonstrate that your ehrenstein function works by calling the function at least three times at the end of basic-datatypes.scm with various inputs.
  • Appropriately decompose your function into smaller functions as you identify different sub-components of the image.
  • Give complete documentation strings for all functions that you write.

Submitting your work

Turn in your completed basic-datatypes.scm file to Gradescope.