This appendix describes a number of coding conventions that we expect you to follow in 15100. Note that failure to follow these guidelines can result in point deductions on programming assignments!
The main purpose of these guidelines is to improve the readability and consistency of your code. Code is read much more often than it is written and inconsistency in style, bad indentation practice, and excessively long lines all make it harder to read.
In some cases, this guide describes features that have not yet been introduced for use in the class (e.g., using iteration and lists instead of enumeration). As a general rule, you can ignore parts of the style guide that rely on language features that have not been described in class. |
Function headers
Each named function definition should have a signature (or type ascription) and purpose comment preceding the function definition. We expect these to have the following format:
(: function : type)
;; a comment describing the purpose of the function
;; and any assumptions about its input that are not
;; reflected in its type
(define (function ...)
...)
Note that the purpose comment comes after the type ascription (not before).
Also, tests should be included following the function being tested.
Commenting
Comments can be classified into what comments and how comments. The former provides a description of what a variable is used for and what a function does, whereas the latter provides a description of how something is computed.
Purpose comments are an example of what comments, as are comments that describe types.
Variable names
A good rule of thumb when choosing a variable or function name is that the length of the name should be proportional to the span of its use. In other words, if the variable is used over most of the program, then its name should be descriptive of its use, whereas a variable that is local to a small function can be short.
You should avoid using names that are the same as commonly used library
functions. In particular, you should never use the names list
or
vector
for variables.
Indentation and line lengths
Racket code should be indented to show the syntactic structure of the
code. Fortunately, DrRacket’s auto-indent support makes it easy to
keep your code properly indented. You can also get the editor to
re-indent your code by either selecting a block of code with the mouse
and hitting the <Tab>
key or by using the Racket → Reindent
or
Racket → Reindent All
menu options.
You should limit the length of any line of code (or comment) to no more than 80 characters. DrRacket has a preference option (under the "Editing/General Editing" tab) for displaying a guide at a given width.
Remember that line lengths matter for comments too!
Where you choose to put line breaks will have an impact on the amount of indentation and length of your lines. In general, you should try to place your line breaks to avoid excessive indentation. For example, put a line break immediately following the parameters of a function definition. For example, the layout
(define (f x)
(if (= x 1)
(+ 1 x)
(- x 1)))
instead of
(define (f x) (if (= x 1)
(+ 1 x)
(- x 1)))
When indenting conditionals, put a break immediately after the cond
; i.e.,
(cond
[(= x 1) (+ 1 x)]
[else (- x 1)])
instead of
(cond [(= x 1) (+ 1 x)]
[else (- x 1)])
Likewise for if
expressions, the layout
(if (= x 1) (+ 1 x) (- x 1))
is preferred over
(if (= x 1) (+ 1 x)
(- x 1))
or
(if
(= x 1)
(+ 1 x)
(- x 1))
Advanced language features
Typed Racket is a large language that inherits many language mechanisms from Racket. In this course, we use a fairly constrained subset of these features so that we can focus on computational patterns, rather than language mechanisms. You should restrict your programs for this class to those features that are presented in lecture or lab, described in this document, or described in an assignment.
Likewise, we will try to introduce all of the library functions that you might reasonably need for this course, but if you want to use a function that we have not introduced, please ask about it on Piazza.
Common programming mistakes
There are a number of common errors that we see students make in their code. We describe these in this section, so that you can avoid them.
Equality testing
Typed Racket provides several polymorphic equality functions (equal?
, eqv?
, and
eq?
). (Note that these functions are currently prohibited by cs151-core anyway.)
You must not use these and instead use the equality test
that is specific to the type of values you are comparing; e.g., use =
for
testing equality of numbers, string=?
for comparing strings,
symbol=?
for comparing symbols, etc..
Comparing booleans
Avoid comparing booleans with #t
and #f
, i.e., instead of
(boolean=? exp #t) ;; WRONG
just write the expression by itself
exp
and instead of
(boolean=? exp #f) ;; WRONG
write
(not exp)
Another common pattern to be avoided is boolean constants in the arms of conditionals. For example, do not write
(if exp #t #f) ;; WRONG
Along similar lines, do not write this equivalent:
(cond
[exp #t]
[else #f])
Instead, write
exp
Likewise, using boolean constants as arguments to and
and or
should
be avoided.
Proper use of type membership tests
Type membership tests should be used to discriminate on union types, but should not be used as a substitute for value testing. For example, instead of using a type test
(integer? (/ a b)) ;; WRONG
write
(= (remainder a b) 0)
Local definitions
Although Racket does allow including a define
in certain contexts where an
expression is required, we use the local
declaration form to clearly define
the scope of the definitions.
For example, instead of
(: f : Integer -> Integer)
(define (f x)
(define y : Integer (* x x)) ;; WRONG
(- y x))
write
(: f : Integer -> Integer)
(define (f x)
(local
{(define y : Integer (* x x))}
(- y x)))
Use iteration instead of static enumeration
The following only applies once lists have been introduced. |
Avoid large conditionals that enumerate many cases when possible. Instead, define tables (lists) and use iteration to do the case analysis. For example, instead of writing
(define (int->dir dir)
(cond
[(= dir 0) 'North]
[(= dir 1) 'NorthEast]
[(= dir 2) 'East]
[(= dir 3) 'SouthEast]
[(= dir 4) 'South]
[(= dir 5) 'SouthWest]
[(= dir 6) 'West]
[else 'NorthWest]))
to implement a mapping from integers to symbols, define a list
(define dir-map
(list 'North 'NorthEast 'East 'SouthEast 'South 'SouthWest 'West 'NorthWest))
and use the list-ref
function to implement the mapping
(list-ref dir-map dir)
Efficiency issues
Use local definitions to avoid expensive redundant computations. In particular, you should never have redundant calls to recursive functions.
Be careful in your use of append
to build lists. For example, instead of using
append
to repeatedly add elements to the end of a list, build the result in reverse
order using cons
and then reverse the result. It is the difference between an
\(O(n)\) and \(O(n^2)\) running time.
Do not use length
to test for empty or singleton lists. For example, instead of
(if (= (length xs) 0) ...)
write
(if (empty? xs) ...)
or use pattern matching.