Code is how we communicate our intentions to a computer, but it is also a written artifact we use to communicate with other humans. In this course, you are communicating with the course staff who are grading your work, so it is to your advantage to make your code as easy to read as possible. In other situations, your code will be read by classmates, collaborators, coworkers, and (maybe most importantly) yourself in the future, after the problem is no longer fresh in your mind. The underlying rationale for any style guide is to help the person reading your code to understand it more easily.
The syntax and semantics of your code says what it does, but good style will also help you communicate why it does what it does. Choosing good variable and function names is crucial for this. Choosing variable names that communicate something about the type of the variable is extremely helpful as well.
Another way of clarifying your intentions is to comment your code well. Note that comments should only communicate information that isn't explicitly available in your code -- don't just summarize what the code does, but add information about why a function is needed or why a particular design choice was made. Keep in mind that under-commented code won't be clear to your readers, but over-commenting can make code cluttered and hard to read.
Important node about comments on homework: In your homework assignments, always start each problem with a comment containing the problem number. This helps us when we are grading your work!
Redundant code is harder to read and harder to debug. Remove any variables or function arguments that are never used. If you find yourself writing the same code twice, think about how you can refactor in order to reuse a single piece of code instead. This could mean streamlining a recursive function, or binding an expression or function to a variable for reuse. (You have probably noticed that the homework problems sometimes build on top of each other.)
Also, redundant calculation can incur performance costs. If an expression is evaluated and its value is needed elsewhere, bind the value to a variable rather than evaluating the expression twice.
A secondary rationale of style guides is to short-circuit pointless arguments; it doesn't
particularly matter if you indent using two spaces or four, but you shouldn't do both.
Choosing one of two equally good alternatives means you can free up the mental cycles
you would spend making that choice every time the situation arises. It also can
sometimes save you from mistakes: for example, if you arbitrarily switch between
camelCase
and snake_case
for variable names, at some point
you'll probably choose wrong and end up with a bug in your code.
Learning how to write good tests for your code is an important skill in software development. There are all sorts of approaches and fancy testing frameworks out there, but all a test needs to be is some evaluation of a piece of your code, the value that you expect that evaluation to have, and a check that those two things are actually the same.
It's a good idea to start writing tests as you work on your solutions rather than after your code is complete, and to run your tests periodically while you work. Not only will you catch errors early, you will also be able to see if you've made any changes that break previously passing tests.
Deciding how much testing is enough is something of an art. Think about one or more "normal" inputs to your functions, then think of a few inputs that could cause problems (empty lists? negative integers?). You could also look at your code and make sure that each important part of it will be evaluated when running your tests. For example, when testing a recursive function, you should make sure your tests exercise the base case and each way in which the function calls itself.
(* this is a
* multiline spanning
* comment *)
Take care to keep your boolean expressions concise. These are just a few examples; there are many more:
if e then true else false
if e then e else false
if e then false else true
if not e then x else y
if x then true else y
if x then y else false
if x then y else true
if x then false else y
e
e
not e
if e then y else x
x orelse y
x andalso y
not x orelse y
not x andalso y
Using unnecessary parentheses makes your code harder to read (and will actually break things
in Racket!). Try to only use parentheses that are syntactically required or that help to
clarify expressions for the reader. For consistency, stick to one style of tuple accessors:
(#1 tup)
or #1 tup
.
if-then-else
expressions are preferred if there are only two possible branches
that can be distinguished with a boolean condition, and the conditions are not matching a
datatype. case
expressions when pattern matching a datatype, or to avoid
nested if
expressions.
Whenever possible, use deep pattern matching instead of nested case
expressions
to make code more readable.
case xs of
[] => true
| x::[] => true
| x::xs' => case xs' of
[] => true
| y::ys' => f x y ys'
case xs of
[] => true
| x::[] => true
| x::(y::ys') => f x y ys'
Nested case
expressions can be appropriate when an additional operation is needed
only in some cases, but the deep patterns should still be used as much as possible.
case xs of
[] => []
| x::xs' => case xs' of
[] => []
| y::ys' => case f x y of
NONE => []
| SOME v => g v
case xs of
[] => []
| x::[] => []
| x::y::ys' => case f x y of
NONE => []
| SOME v => g v
(Notice in this example that the nested case
expression is matching on the result of a function call.)
Think about which scope a value or function is needed in -- if a helper function will only be
invoked within a main function, it makes sense to define it with a let
expression
inside that function. If a value will be used by multiple functions, it may make more sense
to declare it in the global scope.
Don't unnecessarily nest let
expressions. A second binding in a let
expression can refer to a previously bound variable, so there is no need for a second
let
in this case:
let val x = 5
in
let val y = x + 1
in
x + y
end
end
let val x = 5
val y = x + 1
in
x + y
end
When possible, prefer building lists using cons (::
) rather than
append (@
), especially in a recursive context. Since cons evaluates
in constant time, whereas append evaluates in linear time, this can have significant impact on efficiency.
Don't wrap functions unnecessarily. Also, when defining helper functions within a
let
expression, don't pass an argument to the helper for a value that the
helper already has access to in the scope of the main function. Don't use an
accumulator in a recursive helper function if the function is not tail-recursive.
fun f x = g x
fun f x = g y x
fun f x y =
let fun g z y = h z y
in g q y
end
val f = g
val f = g y
fun f x y =
let fun g z = h z y
in g q
end
Note: Most style guidelines from SML continue to apply in Racket. In particular, the sections Simplify boolean expressions, Prefer local scope, and Avoid nested let expressions continue to be relevant. All general style guidelines still apply as well.
Keeping lines of code short is especially important for readability in Racket. Use the following guidelines:
define
should be on its own lineif
and cond
expressions should use one line per condition
or branch (an if
could be all on one line if it is very short)(define x 3) (define y 10)
(foo x
y z)
(if (> x 100) (+ x 101)
(- x 101))
(define (foo x)
(+ x 1)
)
(define x 3)
(define y 10)
(foo x y z)
(if (> x 100) (+ x 101) (- x 101))
OR
(if (> x 100)
(+ x 101)
(- x 101))
(define (foo x)
(+ x 1))
Indentation should be used to line up your code in a way that makes its structure clear. Typically, indent two spaces when nesting an expression,
or align similar expressions (such as cases of a cond
or arguments to a function call) directly under each other.
The easiest way to get correct indentation is to load your code into DrRacket and use the Reindent All command (under the Racket tab; keyboard shortcut Ctrl/Cmd-i)
Use null
to refer to the empty list. Do not sure '()
or
(list)
. ABSOLUTELY do not use ()
; this does not refer
to the empty list in Racket.
Square brackets are essentially equivalent to parentheses in Racket (although if you open an expression with a square bracket, you must also close it
with a square bracket). Traditionally, square brackets are used in let
and cond
expressions and rarely used elsewhere.
(let ((x 100)
(y 101))
(+ x y))
(cond ((> x 100) (list 1 2))
((< x 100) (list 3 4))
(#t null))
(let ([x 100]
[y 101])
(+ x y))
(cond [(> x 100) (list 1 2)]
[(< x 100) (list 3 4)]
[#t null])
Use let
instead of let*
or letrec
for local
bindings unless the specific semantics of let*
or letrec
are needed.