CSE-321 Programming Languages - Standard ML style guide

[ Home | Schedule | Assignments | Software | Resources ] gla@postech Sungwoo Park


In functional programming, it is important to maintain a consistent programming style. A good programming style helps not only the reader understand your code but also yourself better develop the code. This may not sound very convincing, especially to those who dimiss the importance of a good programming style by saying that syntax is something inferior to semantics. The reality, however, is that syntactic beauty plays such a crucial role in programming, as exemplified by Haskell, one of the mainstream functional languages which enforces strict indentation rules for some of its language constructs.

Following are a collection of rules for a good Standard ML programming style. For an educational purpose, all these rules are regarded as mandatory for CSE-321 students unless otherwise noted. Failure to follow these rules may result in penalty for your assignments.

This guide is based upon the style guide for CS321 Data Structures and Functional Programming at Cornell University.

Files

  • 80 column limit: No line of code can have more than 80 columns. Using more than 80 columns causes your code to wrap around to the next line which is devastating to the readability of your code. Ensuring that all your lines fall within the 80 column limit is not something you should do when you have finished programming.
  • No tab characters: Do not use the tab character (0x09). Instead use spaces to control indenting. This is because people may use different values for the tab width.
    In the Vi editor, the following setting in your .vimrc file causes a tab character to be automatically expanded to white spaces:
       set et
  • Begin with a comment: All files must begin with a comment. In other words, the first two characters of the file must be (*. Write the summary of the file in the comment.

Comments

  • Comments go above the code they reference: See the following example:
    (* Sums a list of integers. *)
    val sum = foldl (op +) 0
    
  • Avoid useless comments: Avoid comments that merely repeat the code they reference or state something obvious. Comments should state the invariants, something non-obvious, or references that have more information about the code.
  • Avoid excessive commenting:. Very many or very long comments are more distracting than helpful. Long comments may appear at the top of a file if you wish to explain the overall design of the code. Judicious choice of variable names can also help minimize the need for comments.
  • Line breaks: Unless function declarations within a let block are long, there should be no empty lines within a let block. There should never be an empty line within an expression.
  • Multi-line commenting: Multiline comments can be distinguished from code by preceding each line with a *, as illustrated below. In the Vi editor, the gq command respects * in the beginning of a line when rearranging words in the buffer.
    (* ...
     * ...
     * ... *)
    fun f () = ...
    

Naming and declarations

  • Naming conventions: The following are the preferred rules that are followed by the SML basis and SML/NJ libraries and that we suggest you follow:

    Token Convention Example
    Variables Symbolic or initial lower case. Use embedded caps for multiword names. getItem
    Constructors Initial upper case. Use embedded caps for multiword names. Historical exceptions are nil, true, and false. Rarely are symbolic names like :: used. Node EmptyQueue
    Types All lower case. Use underscores for multiword names. priority_queue
    Signatures All upper case. Use underscores for multiword names. PRIORITY_QUEUE
    Structures Initial upper case. Use embedded caps for multiword names. PriorityQueue
    Functors Same as structure convention, except Fn completes the name. PriorityQueueFn

  • Order of declarations in a structure: When declaring elements in a structure, you should first alias the structures you intend to use, followed by types, and followed by exceptions. Lastly you list all the value declarations for the structure. Here is an example:
    struct
      structure L = List
      type foo = unit
      exception InternalError
      fun first l = L.nth (l, 0)
    end
    

Indenting

  • Indent by two or three spaces: Be consistent.
  • Case expressions should be indented as follows:
    case expr of
      pat1 => ...
    | pat2 => ...
    
  • if expressions should be indented according to one of the following schemes:
    if exp1 then exp2                         if exp1 then
    else if exp3 then exp4                      exp2
    else if exp5 then exp6                    else exp3
         else exp7
    
    if exp1 then exp2 else exp3               if exp1 then exp2
                                              else exp3
    
  • Comments should be indented to the level of the line of code that follows the comment.

Parentheses

  • Over-parenthesizing: Parentheses have many semantic purposes in SML, including constructing tuples, grouping sequences of side-effect expressions, forcing a non-default parse of an expression, and grouping structures for functor arguments. Avoid using unnecessary parantheses when their presence makes your code harder to understand.
  • case expressions: Wrap case expressions with parentheses. This avoids a common error involving nested case expressions. If the case expression is already wrapped by a let ... in ... end block, you can drop the parentheses.
  • Block styles: Blocks of code such as let ... in ... end, struct ... end, and sig ... end should be indented as follows:
    fun foo bar =        fun foo bar =       fun foo bar = let
      let                  let val p = 1       val p = 1
        val p = 1              val q = 1       val q = 1
        val q = 1          in                  in
      in                   bar * (p + q)       bar * (p + q)
        bar * (p + q)    end                 end
      end
    

Pattern Matching

  • No incomplete pattern matches: Incomplete pattern matches are flagged with compiler warnings. Avoid them by using wild card patterns if necessary.
  • Pattern matches in function arguments when possible. Tuples, records, and datatypes can be deconstructed using pattern matching. If you simply deconstruct function arguments before you do anything useful with them, it is better to apply pattern matching in function arguments. Consider these examples:

    Bad Good
    fun f arg1 arg2 = let
      val x = #1 arg1
      val y = #2 arg1
      val z = #1 arg2
    in
      ...
    end
    
    fun f (x, y) (z, _) = ...
    
    fun f arg1 = let
      val x = #foo arg1
      val y = #bar arg1
      val baz = #baz arg1
    in
      ...
    end
    
    fun f {foo=x, bar=y, baz}
    

  • Avoid unnecessary projections. Prefer pattern matching to projections with function arguments or value declarations. Here is an example for pattern matching with value declarations.

    Bad Good
    let
      val v = someFunction ()
      val x = #1 v
      val y = #2 v
    in
      x+y
    end
    
    let
      val (x,y) = someFunction ()
    in
      x+y
    end
    

  • Combine nested case expressions: Rather than nested case expressions, you can combine them by pattern matching against a tuple, provided the tests in the case expressions are independent. Here is an example:

    Bad Good
    let
      val d = Date.fromTimeLocal (Time.now ())
    in
      case Date.month d of
        Date.Jan => (case Date.day d of
                       1 => print "Happy New Year"
                     | _ => ())
      | Date.Jul => (case Date.day d of
                       4 => print "Happy Independence Day"
                     | _ => ())
      | Date.Oct => (case Date.day d of
                       10 => print "Happy Metric Day"
                     | _ => ())
    end
    
    let
      val d = Date.fromTimeLocal (Time.now ())
    in
      case (Date.month d, Date.day d) of
        (Date.Jan, 1) => print "Happy New Year"
      | (Date.Jul, 4) => print "Happy Independence Day"
      | (Date.Oct, 10) => print "Happy Metric Day"
      | _ => ()
    end
    

  • Avoid the use valOf, hd, or tl. The functions valOf, hd, and tl are used to deconstruct option types and list types. However, they raise exceptions on certain inputs. You should avoid these functions altogether. It is usually easy to achieve the same effect with pattern matching. If you cannot manage to avoid them, you should handle any exceptions that they might raise.

Factoring

  • Do not factor unnecessarily.

    Bad Good
    let
      val x = TextIO.inputLine TextIO.stdIn
    in
      case x of
        ...
    end
    
    case TextIO.inputLine TextIO.stdIn of
      ...
    
    (* y is not a large expression *)
    let val x = y * y in x + z end
    
    y * y + z
    

Verbosity

  • Do not rewrite library functions -- use them!. The basis library and the SML/NJ library have a great number of functions and data structures -- use them! For example, a good use of list functions such as List.filter, List.map, List.app, List.foldl, and List.foldr can greatly simplify your code using lists.
  • Misusing if expressions. If the type of an if expression is bool, but you should not use the if expression at all. Consider the following examples:

    Bad Good
    if e then true else false     e
    if e then false else true not e
    if e then e else false e
    if not e then x else y if e then y else x
    if x then true else y x orelse y
    if x then y else false x andalso y
    if x then false else y not x andalso y
    if x then y else true not x orelse y

  • Misusing case expressions. The case expression is misused in two common situations. First case should never be used in place of an if expression (that's why if exists).
    case e of
      true => x
    | false => y
    
    if e then x else y
    
    The latter is better. Another situation where if expressions are preferred to case expressions is as follows:
    case e of
      c => x   (* c is a constant value *)
    | _ => y
    
    if e = c then x else y
    
    The latter is definitely better. The other misuse is using case when pattern matching with a val declaration is enough. Consider the following example:
    val x = case expr of (y, z) => y
    
    val (x, _) = expr
    
    The latter is better.
  • Other common misuses. Here are some other common mistakes to watch out for:

    Bad Good
    l::nil [l]
    l::[] [l]
    length + 0 length
    length * 1 length
    (big exp) * (the same big exp) let val x = (big exp) in x * x end
    if x then f a b c1 else f a b c2 f a b (if x then c1 else c2)
    String.compare (x, y) = EQUAL x = y
    String.compare (x, y) = LESS x < y
    String.compare (x, y) = GREATER x > y
    Int.compare (x, y) = EQUAL x = y
    Int.compare (x, y) = LESS x < y
    Int.compare (x, y) = GREATER x > y
    Int.sign x = ~1 x < 0
    Int.sign x = 0 x = 0
    Int.sign x = 1 x > 0

  • Do not rewrap functions. When passing a function as an argument to another function, do not rewrap the function unnecessarily. Here's an example:
    List.map (fn x => Math.sqrt x) [1.0, 4.0, 9.0, 16.0]
    
    List.map Math.sqrt [1.0, 4.0, 9.0, 16.0]
    
    The latter is better. Another case for rewrapping a function is often associated with infix binary operators. To prevent rewrapping the binary operator, use the op keyword as in the following example:
    foldl (fn (x, y) => x + y) 0
    
    foldl (op +) 0
    
    The latter is better.
  • Do not needlessly nest let expressions. Multiple declarations may occur in the first block of a let ... in ... end expression. The bindings are performed sequentially, so you may use a name bound earlier in the same block. Consider the following example:
    let
      val x = 1
    in
      let
        val y = x + 1
      in
        x + y
      end
    end
    
    let
      val x = 1
      val y = x + 1
    in
      x + y
    end
    
    The latter is better.

[ Home | Schedule | Assignments | Software | Resources ] gla@postech Sungwoo Park