Last updated: Sat, 11 Apr 2015 13:03:10 -0400
Style and formatting conventions are key to writing readable, maintainable programs. If everyone follows the same set of rules, then all programs become easier to understand because everyone starts with the same expectations and assumptions.
Thus, every programming language will have its own style conventions; organizations and projects often establish their own rules as well. To avoid confusing readers of your programs, it is important to follow the established conventions of your programming language and organization.
This page describes the style rules for this course. Failure to follow any of these rules will result in grade penalties.
Don’t change given requirements. If you are given any data definitions, function headers, function names, function argument names, etc., your solution must include them exactly as given.
We will not listen to any complaints about lost points resulting from changing the given requirements.
Line Width Do not exceed 80 columns per line.
- 1 Function, 1 Task Keep functions small (max 5-8 lines) and limit them to one clear task. Utilize function composition to combine smaller tasks when appropriate.
(define (ball-tick b) (if (and (≤ YUP (where b) YLO) (or (≤ (ball-x b) XWALL (+ (ball-x b) (ball-dx b))) (>= (ball-x b) XWALL (+ (ball-x b) (ball-dx b))))) (make-ball (- (* 2 XWALL) (ball-x (straight b 3))) (ball-y (straight b 3)) (- (ball-dx (straight b 3))) (ball-dy (straight b 3))) (straight b 3)))
- D.R.Y. Avoid repeated code. Strive for a single point of control.
- Abstract common tasks into separate functions when it makes sense.
contains opportunity for abstraction
; str-lengths : ListOf<String> -> ListOf<Natural> (define (str-lengths str-lst) (cond [(empty? str-lst) empty] [else (cons (string-length (first str-lst)) (str-lengths (rest str-lst)))])) (define KEY-LENS (str-lengths '("up" "up" "down" "b" "a"))) (define (double nums) (cond [(empty? nums) empty] [else (cons (* 2 (first nums)) (double (rest nums)))])) (define CIRCUMFERENCES (double RADIUSES))
Define global constants instead of hard-coding "magical" numbers (or strings, etc.).Label them with units when appropriate.
When defining itemization data comprised of constant values, it’s a good idea to define global constants, and predicate functions that identify each of these constants. This gives you a single point of control, in case you want to change the data definition later.
TrafficLight: too many hard-coded string values (bad)
TrafficLight: with global constants (good)
; A TrafficLight is one of: ; - "red" ; - "yellow" ; - "green" (define RED "red") (define YELLOW "yellow") (define GREEN "green") ; TrafficLight -> Boolean ; Returns true if the given TrafficLight state tl matches ; the color indicated by the predicate name. (define (red? tl) (string=? tl RED)) (define (yellow? tl) (string=? tl YELLOW)) (define (green? tl) (string=? tl GREEN)) (define (traffic-fn1 tl) (cond [(red? tl) ...] [(yellow? tl) ...] [(green? tl) ...])) (define (traffic-fn2 tl) (cond [(red? tl) ...] [(yellow? tl) ...] [(green? tl) ...])) ...
Defining global variables for string constants also helps prevent typos, since DrRacket will tell you if you mistype a variable name (of course, you have to spell the initial string constant correctly but you only have to get one place right rather than many).
Defining global constants for Data Examples for use in tests also helps to reduce code repetition and errors.
- Do not take code duplication elimination too far (e.g., when it begins to hinder readability). Above all, a function must have one distinct task. A good indication of code duplication elimination gone-too-far is when you have trouble naming your new function.
excessive duplicate code elimination
; diff+sqr : Real Real -> NonNegReal ; Computes square of the difference of x1 and x2. ; STRATEGY: function composition (define (diff+sqr x1 x2) (sqr (- x1 x2))) ; dist : Real Real Real Real -> NonNegReal ; Computes the Euclidean distance between points (x1,y1) and (x2,y2). ; STRATEGY: function composition (define (dist x1 y2 x2 y2) (sqrt (+ (diff+sqr x1 x2) (diff+sqr y1 y2))))
excessive eliminination of repeated code
(define (do-if test? arg1 arg2 fn1 fn2) (if (test? arg1) (fn1 arg1 arg2) (fn2 arg1 arg2))) (define (solve-puzzle board pieces) (do-if dead-end? board pieces fail try-next-move)) (define (less-taxes2 income ignored) (less-taxes income)) (define (more-taxes2 income ignored) (more-taxes income)) (define (compute-taxes income) (do-if lower-bracket? income 'dummy less-taxes2 more-taxes2))
- Avoid Extraneous Code
Omit unused arguments.
- Avoid conditionals that just return true or false.
- Don’t build unnecessary data structures.
- Don’t re-implement (parts of) existing functions.
Indenting Follow Racket’s indenting conventions. DrRacket’s Racket -> Reindent All menu command will automatically indent your program properly.
- Line Breaks
Put function headers on their own line.
if and cond branches each get a separate line.
Parentheses Do not put open or closing parentheses on a separate line. Closing parentheses should accompany the last line of code and should not have any spaces in between.
- Spacing For parentheses that do not start or end a line, always put a space before a group of open parentheses and after a group of closing parentheses.
In Racket, names are case-sensitive, so be aware of this.
Readable programs must have descriptive function and variable names. Name things in terms of information whenever possible.
<some-function-name> and <some-function-name>-helper
<some-function-name>, <some-function-name>2, and <some-function-name>3
When you have a group of related functions, they should have a thoughtful, consistent naming scheme that conveys their relation to each other. A common example is a series of data decomposition functions that performs a task, separated by the kind of data.
(define-struct world (balls bats gloves)) (define (next-world w) (make-world (next-balls (world-balls w)) (next-bats (world-bats w)) (next-gloves (world-gloves w)))) (define (next-balls balls) (... (next-ball ball) ...)) (define (next-bats bats) (... (next-bat bat) ...)) (define (next-gloves gloves) (... (next-glove glove) ...))
(define-struct world (balls bats gloves)) (define (world-tick w) (make-world (tick-helper1 (world-balls w)) (tick-helper2 (world-bats w)) (tick-helper3 (world-gloves w)))) (define (tick-helper1 x) (... (tick4 x) ...)) (define (tick-helper2 y) (... (tick5 y) ...)) (define (tick-helper3 z) (... (tick6 z) ...))
Other naming conventions:
Data definition names are in CamelCase, e.g., KeyEvent, World, ListOfNumber.
Use hyphens ("-") to separate words in function and variable names, e.g., world-after-tick. Do not use underscores ("_").
Predicates end in a question mark ("?"). e.g., empty?, zero?.
Variables representing Maybe<X> values should end with a /#f prefix.
The Program Design Recipe page explains the steps you should follow when writing programs in this course. The following style rules refine the recipe steps to improve program readability.
Put all data definitions at the top of a file where they are easily found.
Data definition names start with a capital letter and are in CamelCase, e.g., KeyEvent, World, ListOfNumber.
- Declare alias data definitions where appropriate, so that data definitions convey information whenever possible. Use unit names directly when you think it improves readability.
; A Ball is a (make-ball Real Real Integer)
; A Velocity is an Integer, in pixels/sec. ; A Ball is a (make-ball Coordinate Coordinate Velocity)
directly using unit names may be better in some contexts
; A MPH is an Integer, representing a speed in miles per hour. ; A Ball is a (make-ball Coordinate Coordinate MPH)This means that your data definitions should rarely use abstract kinds of data like numbers or strings. Give it a more appropriate name.
Note: The textbook occasionally misses opportunities to use aliases, so be aware of this while reading.
On the other hand, if you happen to be writing math functions (e.g., factorial), or when a unit of measurement is less applicable (e.g., counting things), then using one of the numerical Pre-defined Data Definitions is acceptable.
You may use interval notation when defining data definitions for intervals of numbers.
; A Grade is one of the following PosReal intervals: ; - [0 70) ; - [70 80) ; - [80 90) ; - [90 100]
When defining itemization data, put the easiest-to-distinguish choices first. This makes it easier to write functions that process this kind of data. See Template for Itemization Data for more information.
Not all data definitions require invariants so add them when appropriate.
- Data definitions should be as precise as possible. Therefore, declare data definition invariants only when it’s not possible to capture the invariant in the main part of the data definition. In other words, because it is less formal, use English prose to describe the data definition only as a last resort.
; An NEListOfNum is ListOf<Num>. ; WHERE: the list has at least one number.
; An NEListOfNum is a (cons Num ListOf<Num>)
; A SetOf<Num> is a ListOf<Num>. ; WHERE: there are no duplicate elements.
; A SortedListOf<Num> is a ListOf<Num>. ; WHERE: the list is sorted in ascending order.
- Sometimes, a more informal description that interleaves a data definition with its invariant is more readable and is acceptable.
; An Evens is an Integer, ; WHERE: where for each Evens n, (zero? (modulo n 2)) is true.
even integers, better
; The Evens are the even Integers.
; A 1String is a String, ; WHERE: where the string has length 1.
1-char strings, better
; A 1String is all the Strings of length 1.
It may be helpful to label an interpretation with INTERPRETATION: or INTERP. unless it is very short.
Occasionally, the information represented by a data definition may not be known. In this scenario, no interpretation is needed. Data definitions that are parameterized over other data definitions, like ListOf<X>, fall into this category. Data definitions that represent a subset of other abstract data, like Evens or 1String above, are other examples.
Otherwise, a data definition must always have an interpretation.
Follow the upper-case convention for global variables when creating data examples.
You almost always need to define examples for use in tests anyways, so there is no excuse for not having them.
Omit data examples at your own risk. Your grader is the final arbiter of whether they are required in a particular piece of code. If your grader cannot understand a data definition and additional data examples would have helped, you will lose points.
All functions must have a purpose statement.
A purpose statement mentions a function’s arguments by name.
A purpose statement describes at a high-level how the result is computed from its inputs.
Concision (without loss of clarity) is crucial. Write and repeatedly edit purpose statements until they are short as possible without sacrificing clarity and accuracy.
If a purpose statement is longer than 2-3 (80 column) lines, it’s probably either unclear and needs to be rewritten, or the function could be broken into smaller functions.
Most of the time, a concise purpose statement is a clear purpose statement.
- You may occasionally combine purpose statements for a group of related functions that have an identical signature. However, you must make sure that this purpose statement actually applies to all the functions. In the combined signature, where the function name would normally appear, describe the functions and enclose the description in angle brackets to indicate a group of functions.
Combine purpose statements at your own risk. Your grader is the final arbiter of whether a particular purpose statement is necessary. If your grader cannot understand your function and a purpose statement would have helped, you will lose points.
Not all functions require argument invariants so add them when appropriate.
Declare argument invariants only when they would not make sense as part of a data definition or as data invariants. For example, argument invariants often specify a relation between two or more different arguments of a function.
- Function examples are almost always helpful. Occasionally, however, other parts of the Function Specification are a more direct means of conveying the function’s behavior. Thus, function examples but may be omitted in the following cases (these exceptions do not affect Testing requirements):
The function is a short function composition and all the called functions have proper examples or are library functions.
The function is defined via a lambda.
- Other times, concrete code examples may be awkward. In the following cases, code examples may be replaced with short English prose.
The function produces an image.
The function produces an object.
In general, you should think hard about how your examples complement other parts of your Function Specification. You may be asked about this during your code walk.For instance, the example for the area function below doesn’t convey much beyond what the signature and purpose statement already describe since the function is small, so it’s probably fine to leave it out.
Function examples are great for concisely and precisely describing certain edge cases.
(define VELOCITY 10) ; pixels/tick (define WALL-X 100) ; pixels ; Coordinate -> Coordinate ; Computes the new x coordinate after one tick. ; If the new coordinate would exceed WALL-X, the ; new coordinate is exactly at the wall. (begin-for-test (check-equal? (x-tick (add1 (- WALL-X VELOCITY))) WALL-X "x would move past wall")) ; STRATEGY: function composition (define (x-tick x) (if (> (+ x VELOCITY) WALL-X) WALL-X (+ x VELOCITY)))Notice how the example precisely clarifies a behavior that was more difficult and verbose to describe in the purpose statement.
Omit function examples at your own risk. Your grader is the final arbiter of whether a examples are necessary for a specific function. If your grader cannot understand your function and examples would have helped, you will lose points.
All functions must declare a strategy.
A data decomposition function must declare a variable name and a data definition name:
data decomposition strategy example
; A Ball is a (make-ball Coordinate Velocity) (define-struct ball (x vel)) ; next-ball : Ball -> Ball ; Computes the next ball state. ; STRATEGY: data decomposition on b : Ball (define (next-ball b) (... (ball-x b) ... (ball-vel b) ...))
Once the strategy of a function is declared, the body of the function may only use the pieces of data described in the template. In particular, the function may not decompose another kind of data not mentioned in the strategy.
A function’s implementation must follow it’s declared strategy.
You may not use cond anywhere in your code unless it comes from a template (which means you are decomposing itemization data).
4.3.1 "Inline" decomposition of Boolean data
One Boolean may be decomposed inline without declaring it in the strategy.
strategy matches function
; An BallX is a Coordinate, split into the following intervals: ; - < 0 ; - [0 WIDTH] ; - > WIDTH ; A Ball is a (make-ball BallX) (define-struct ball (x)) ; next-ball : Ball -> Ball ; STRATEGY: Data Decomposition on b : Ball (define (next-ball b) (make-ball (next-ballx (ball-x b)))) ; next-ballx : BallX -> BallX ; STRATEGY: data decomposition on x : BallX (define (next-ballx x) (cond [(< x 0) (left-bounce x)] [(<= 0 x WIDTH) (go-straight x)] [else (right-bounce x)]))
strategy matches function
; A Ball is a (make-ball Coordinate) (define-struct ball (x)) ; next-ball : Ball -> Ball ; STRATEGY: data decomposition on b : Ball (define (next-ball b) (make-ball (if (x-hits-wall? (ball-x b)) (x-bounce (ball-x b))) (x-straight (ball-x b))))
A function should encapsulate one distinct task. Thus, decomposing more than one of a function’s arguments typically makes the function less clear and less readable.
Double decomposition is acceptable, however, in some specific cases, described below. Double decomposing functions must declare all the decomposed kinds of data in the design strategy.
two compound data arguments, where pieces from both are required at the same time
- operating on two lists of the same length
; list-merge : (X X -> Y) ListOf<X> ListOf<X> -> ListOf<Y> ; Combines the elements of lst1 and lst2 using function f ; and returns the results in a new list. ; WHERE: (length lst1) == (length lst2) ; STRATEGY: data decomposition on lst1, lst2 : ListOf<X> (define (list-merge lst1 lst2 f) (cond [(empty? lst1) empty] [else (cons (f (first lst1) (first lst2)) (list-merge (rest lst1) (rest lst2)))]))
See Chapter 26 of the textbook for more guidelines on this kind of double decomposition.
At a minimum, every program must have 100% code coverage, according to the DrRacket code coverage tool.
Do not paste and save images directly into the DrRacket buffer (even though some textbook examples do this). Load from file (using bitmap, etc.) if necessary. When testing image-producing functions, generate the expected output programmatically.
Class names are capitalized and have a % suffix, for example Robot%.
Interface names are capitalized and have a <%> suffix, for example Robot<%>.
Any method from an interface must be implemented with define/public.
You may add any methods to any interface we give you. However, do not change any of the given methods. Also, you should strive to minimize any publicly-visible interface.
If you add a method to an interface for testing purposes only, prefix it with testing:
NOTE: A key difference with Classes and Interfaces is that defining them implicitly defines a data definition. This is consistent with many OO languages where the name of a class or interface also represents a type. For example, defining a Shape<%> interface means you may use Shape<%> in a method signature. This is different from before, where an analogous (define-struct shape ...) definition did not define an implicit data definition. We required an additional, explicit Shape data definition, i.e., ; A Shape is a (make-shape ...), if you want to use shapes in a function signature.
All fields in a class definition (those defined with init-fields) must be annotated with their data definition and an interpretation.
- Each method declaration in an interface:
- Each method implementation in a class definition:
must repeat the signature, possibly refining it to be more specific for that class; for example, the method may return a concrete class rather than a general interface
do not need a purpose statement, unless you wish to add further detail specific to that implementation, or unless you think it is helpful to have a reminder of the purpose statement.
should have examples if it’s helpful to explain the behavior of the method; the examples should be described informally rather than as formal tests; if your grader cannot understand a method’s behavior and additional examples would have helped, you will lose points
it is not necessary to declare data decomposition of the class itself when you’re only using the fields of the object; if you are decomposing any field or argument, you should declare data decomposition as the strategy; if you’re not decomposing any data, you don’t need to write a strategy
Effectful methods, such as methods that mutate objects, have additional style conventions, so that the effects are always evident to code readers.
An effectful method’s signature should typically return Void, to indicate no return value.
An effectful method must have an EFFECT: statement as part of the method’s Function Specification.
If a method mutates an object, it’s name must have a ! suffix.
If you add a method to an interface for testing purposes only, prefix it with testing:
- 100% code coverage is still required. However, with full Racket, test coverage may not be enabled by default. Enable it by:
selecting Choose Language ... from the menu in the bottom-left of the DrRacket window,
selecting Show Details,
and selecting Syntactic test suite coverage.