--- title: Pyret Style (Testing, Design, and Clarity) Guide tags: Documentation, 2018 --- # Pyret Style (Testing, Design, and Clarity) Guide ## Examples (automated tests) 1. All functions, including helper functions, require examples (except for functions that return`Image`). They serve as a form of documentation. Examples are written in `where`-blocks. 2. Examples should generally be written *before* you start writing your function. Writing good examples will help you understand how your function should behave on different inputs. 3. Examples should illustrate interesting variations in either the inputs or the function behavior. You do not need to test every input or every input combination (indeed, there are usually infinite possible combinations!), but you should illustrate different cases or input scenarios that your function might have to handle. **Unless stated otherwise, please use `where-` blocks instead of `check-` blocks.** Both types of blocks run automated tests of your functions, but `where-` blocks are syntactically attached to each function, so they are easier for the graders to find. ### Guidelines for developing examples #### Cover Edge Cases Edge cases are inputs at the boundaries within the space of inputs. For instance, a function that checks for "numbers between 4 and 8" or "strings with at least 3 characters" has boundaries. Other common boundary values are empty strings, the number 0, or an empty list (unless those aren't meaningful for the specific problem). #### Think about meaningful variations in the input based on what it represents Imagine that you've been asked to use a string to represent #### Ignore unexpected inputs unless you're told otherwise As an example, let's consider a function `compare` that takes two numerical inputs `x` and `y` and returns a string. It should return `"way less"` if `x` is smaller than `y - 2`, `"about the same"` if `x` is between `y - 2` and `y + 2` (inclusive) and `"way more"` if `x` is greater than `y + 2`. Here's a set of good examples for this function. ``` fun compare(x :: Number, y :: Number) -> String: doc: "decribe the relationship between input numbers x and y" ... # function body elided where: compare(1, 40) is "way less" compare(0, 3) is "way less" compare(8, 10) is "about the same" compare(1, 1) is "about the same" compare(0, 1) is "about the same" compare(17, 20) is "way more" end ``` Notice that these examples cover each interesting "case" of the program. They also include edge cases testing the boundaries--for instance, `compare(8, 10)` is an edge case, since it's at the boundary between the `"way less"` and `"about the same"` cases. For most assignments, you can get a substantial amount of partial credit if you write good examples for a function even if your function doesn't work correctly. When writing examples before implementing a function, it may be helpful to include `because` clauses as a way of figuring out what the function body could look like: ``` fun times-four(x :: Number) -> Number: doc: "multiplies input x by four" ... # function body elided where: times-four(3) is 12 because 3 * 4 times-four(0) is 0 because 0 * 4 times-four(-1) is -4 because -1 * 4 end ``` Unless an assignment says otherwise, `because` clauses are not required--but they might be useful to you! ## Design 1. Use constants to capture relationships between values. For example, if you have `rectangle(500, 300, "solid", "green")` but mean for the height to be a scaled fraction of the width, you should instead write: ```ruby width = 500 rectangle(width, 3/5 * width, "solid", "green") ``` 2. Use helper functions to configure an expression that appears multiple times with slightly different arguments. For example, ```ruby stairs = beside-align("bottom", rectangle(50, 50, "solid", "gray"), beside-align("bottom", rectangle(50, 100, "solid", "gray"), beside-align("bottom", rectangle(50, 150, "solid", "gray"), rectangle(50, 200, "solid", "gray")))) ``` could be rewritten as: ``` stair-width = 50 fun stair(height :: Number) -> Image: doc: "make an individual stair given its height" rectangle(stair-width, height, "solid", "gray") end stairs = beside-align("bottom", stair(50), beside-align("bottom", stair(100), beside-align("bottom", stair(150), stair(200)))) ``` 3. Make sure your helper functions and constants are not redundant in light of existing built-in functions. For example, there is no point in making this function: ``` fun string-lower(s :: String) -> String: string-to-lower(s) end ``` since you could always use `string-to-lower` instead. ## Clarity 0. To help your TAs find your work when grading, please annotate each task (code and written reflections) with a small comment, e.g. ``` # Task 1 fun my-func(...) ... end # Task 2 # I believe this happened because... ``` 1. Write docstrings for all functions, including helper functions. A good docstring gives a description of the function, including its input(s) and output. Ideally, by looking at the docstring you know what the function does and how to use it without looking at the function body itself. ``` fun three-stripes(bot-col :: String, mid-col :: String, top-col :: String) -> Image: doc: ```makes a rectangular flag with three stripes. the inputs describe the stripes' colors, bottom-to-top.``` ... end ``` :::warning ## Bad docstring descriptions 1. `doc: "helper function for flag making"` 2. `doc: "output the right image for this problem"` 3. `doc: "uses above to stack images"` - how your function body actually works is irrelevant to the docstring. ::: 2. Give constants and helper functions useful names. ``` n1 = 3.1415 n2 = 500 n3 = 30 fun helper(x): x + 3 end ``` should be rewritten as (for example) ``` pi = 3.1415 flag-width = 500 circle-radius = 30 fun add-3(x): doc: "adds three to its input x" x + 3 end ``` 3. All functions require type annotations on both inputs and output. 4. Names of constants and functions should be lower case and dash separated. For configuration constants (for example the height of a character) it is acceptable to use all caps dash-separated names. ``` # Good Pyret Style: flag-width = 500 width-to-height-ratio = 3/5 CHARACTER-HEIGHT = 40 # Bad Pyret Style: flag_width = 500 widthHeightRatio = 3/5 CharacterHeight = 40 ``` 5. Keep lines under 80 characters. You will at some point see a vertical dashed blue line in Pyret. If you see this line, your lines are too long and you need to add linebreaks (press `enter` at places in your code). The 80 character limit applies even when you have long strings and/or docstrings. Long docstrings can be written using backticks (\`): ``` fun f(x :: Number) doc: ```return 2; ignore inputs, always outputs 2 no matter what, this function is going to return 2``` 2 end ``` 6. Indent your code properly. You can do this by pressing `ctrl-a` then `tab` on Windows and Linux, or `cmd-a` then `tab` on Mac. 7. Do not use nested functions. A function declaration shouldn't happen inside the body of another function. Nested functions are difficult to test and make the code harder to read. When we reach `Table`s in the course, the textbook will have examples of nested functions. We will introduce a different syntax (and post an addendum to the textbook reading) so that we can avoid the use of nested functions. :::warning ## Bad use of a helper function with nesting ``` fun f(x :: Number) -> Number: doc: "Returns the value (2 * x) + 1" fun mul(n :: Number) -> Number: doc: "multiplies x by n" x * n end mul(2) + 1 end ``` ::: To rewrite the above example without nesting, add an input to `g` so that it can have access to `x` without being nested: ``` fun mul(n :: Number, to-mul :: Number) -> Number: doc: "multiples to-mul by n" n * to-mul end fun f(x :: Number) -> Number: doc: "returns the value (2 * x) + 1" mul(2, x) + 1 end ```