Programming Languages: Style Guide

CSE 341: Programming Languages

Overview

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.

General style

Clarify your intentions

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!

Avoid redundant code

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.

Be consistent

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.

Testing

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.

SML style

Clarify multi-line comments

Add an asterisk at the start of each line of a multi-line comment to improve readability:
(* this is a 
 * multiline spanning
 * comment *)

Simplify boolean expressions

Take care to keep your boolean expressions concise. These are just a few examples; there are many more:

Instead of...

  1. if e then true else false
  2. if e then e else false
  3. if e then false else true
  4. if not e then x else y
  5. if x then true else y
  6. if x then y else false
  7. if x then y else true
  8. if x then false else y

Use...

  1. e
  2. e
  3. not e
  4. if e then y else x
  5. x orelse y
  6. x andalso y
  7. not x orelse y
  8. not x andalso y

Limit unnecessary parentheses

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.

Choose if-then-else v. case carefully

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.

Avoid nested case expressions

Whenever possible, use deep pattern matching instead of nested case expressions to make code more readable.

Instead of...

case xs of
    [] => true
  | x::[] => true    
  | x::xs' => case xs' of
                  [] => true
                | y::ys' => f x y ys'

Use...

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.

Instead of...

case xs of
    [] => []
  | x::xs' => case xs' of
                  [] => []
                | y::ys' => case f x y of
                                NONE => []
                              | SOME v => g v
            

Use...

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.)

Prefer local scope

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.

Avoid nested let expressions

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:

Instead of...

let val x = 5
  in 
    let val y = x + 1
      in
        x + y
    end
end

Use...

let val x = 5
    val y = x + 1
  in
    x + y
end

Prefer cons to append

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.

Minimize function definitions

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.

Instead of...

  1. fun f x = g x
  2. fun f x = g y x
  3. fun f x y = 
        let fun g z y = h z y
        in g q y
        end

Use...

  1. val f = g
  2. val f = g y
  3. fun f x y = 
        let fun g z = h z y
        in g q 
        end

Racket Style

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.

Use line breaks effectively

Keeping lines of code short is especially important for readability in Racket. Use the following guidelines:

  • Each define should be on its own line
  • Arguments to a function call should either be all on a single line or each on its own line
  • if and cond expressions should use one line per condition or branch (an if could be all on one line if it is very short)
  • A closing (right) parentheses should never be on its on line, and should never be the first character on a line

Instead of...

  1. (define x 3) (define y 10)
  2. (foo x
         y z)
  3. (if (> x 100) (+ x 101)
                  (- x 101))
  4. (define (foo x)
      (+ x 1)
    )

Use...

  1. (define x 3)
    (define y 10)
  2. (foo x y z)
  3. (if (> x 100) (+ x 101) (- x 101))
    OR
    (if (> x 100) 
        (+ x 101)
        (- x 101))
  4. (define (foo x)
      (+ x 1))

Indent properly

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 represent the empty list

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.

Use square brackets appropriately

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.

Instead of...

(let ((x 100)
      (y 101))
  (+ x y))

(cond ((> x 100) (list 1 2))
      ((< x 100) (list 3 4))
      (#t null))

Use...

(let ([x 100]
      [y 101])
  (+ x y))

(cond [(> x 100) (list 1 2)]
      [(< x 100) (list 3 4)]
      [#t null])

Prefer let for local bindings

Use let instead of let* or letrec for local bindings unless the specific semantics of let* or letrec are needed.