CS312 SML Style Guide
CS312 SML Style Guide
htm
File Submission
80 Column Limit. No line of code may have more than 80 columns. Using more than 80 columns causes your
code to wrap around to the next line, which is devastating to readability. Our installation of Emacs will give you a
warning if you try to save a file with more than 80 columns.
No Tab Characters. Do not use the tab character (0x09). Instead, use spaces to control indenting. The Emacs
package from this website avoids using tabs (with the exception of pasting text from the clipboard or kill ring). This is
because the width of a tab is not uniform across all computers, and what looks good on your machine may look
terrible on mine, especially if you have mixed spaces and tabs.
Code Must Compile. Any code you submit must compile under SML/NJ without errors or warnings. If it does not
compile, we will not grade it. That means you will not receive any points for that problem. There is no excuse for it to
not compile. Never submit anything that you have changed, no matter how small the change, without checking that it
still compiles. You should treat compiler warnings as errors.
Comments
Avoid Useless Comments. Avoid comments that merely repeat the code they reference or state the obvious.
Comments should state the invariants, the non-obvious, or any references that have more information about the code.
Avoid Over-commenting. Very many or very long comments in the code body 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 or refer to any
sources that have more information about the algorithms or data structures. All other comments in the file should be
as short as possible. A good place for a comment is just before a function declaration. Judicious choice of variable
names can help minimize the need for comments.
Line Breaks. Empty lines should only be included between value declarations within a struct block, especially
between function declarations. It is not necessary to put empty lines between other declarations unless you are
separating the different types of declarations (such as structures, types, exceptions and values). 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. When comments are printed on paper, the reader lacks the advantage of color
highlighting performed by an editor such as Emacs. Multiline comments can be distinguished from code by
preceding each line of the comment with a * similar to the following:
1 of 8 2/15/2021, 3:27 PM
CS312 SML Style Guide https://www.cs.cornell.edu/courses/cs312/2008sp/handouts/style.htm
Naming Conventions. The best way to tell at a glance something about the type of a variable is to use the standard
SML naming conventions. The following are the preferred rules that are followed by the SML basis and SML/NJ
libraries:
let
val d = Date.fromTimeLocal(Time.now())
val m = Date.minute d
val s = Date.second d
fun f n = (n mod 3) = 0
in
List.filter f [m,s]
end
Type Annotations. Top-level functions and values should always be declared with types. Consider the difference
bewteen the following:
Avoid Global Mutable Variables. Mutable values should be local to closures and almost never declared as a
structure's value. Global mutable values cause many problems. First, it is difficult to ensure that the mutable value is
in the proper state, since it might have been modified outside the function or by a previous execution of the
algorithm. This is especially problematic with concurrent threads. Second, and more importantly, having global
mutable values makes it more likely that your code is nonreentrant. Without proper knowledge of the ramifications,
declaring global mutable values can extend beyond bad style to incorrect code.
When to Rename Variables. You should rarely need to rename values, in fact this is a sure way to obfuscate
code. Renaming a value should be backed up with a very good reason. One instance where renaming a variable is
common and encouraged is when aliasing structures. In these cases, other structures used by functions within the
current structure are aliased to one or two letter variables at the top of the struct block. This serves two purposes:
it shortens the name of the structure and it documents the structures you use. Here is an example:
struct
2 of 8 2/15/2021, 3:27 PM
CS312 SML Style Guide https://www.cs.cornell.edu/courses/cs312/2008sp/handouts/style.htm
structure H = HashTable
structure T = TextIO
structure A = Array
...
end
Order of Declarations in a Structure. When declaring elements in a structure, you should first alias the structures
you intend to use, followed by the types, followed by exceptions, and lastly list all the value declarations for the
structure. Here is an example:
struct
structure L = List
type foo = unit
exception InternalError
fun first list = L.nth(list,0)
end
Every declaration within the structure should be indented the same amount.
Indenting
case expr of
pat1 => ...
| pat2 => ...
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 ML, including constructing tuples, grouping
sequences of side-effect expressions, forcing a non-default parse of an expression, and grouping structures for
functor arguments. Their usage is very different from C or Java. 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.
Alternative Block Styles. Blocks of code such as let...in...end, struct...end, and sig...end should be
indented as follows. There are several alternative styles to choose from.
fun foo bar = fun foo bar = fun foo bar = let
3 of 8 2/15/2021, 3:27 PM
CS312 SML Style Guide https://www.cs.cornell.edu/courses/cs312/2008sp/handouts/style.htm
Pattern Matching
No Incomplete Pattern Matches. Incomplete pattern matches are flagged with compiler warnings, which are
tantamount to errors for grading purposes. Thus, if your program exhibits this behavior, the problem will get no
points.
Pattern Match in the Function Arguments When Possible. Tuples, records and datatypes can be
deconstructed using pattern matching. If you simply deconstruct the function argument before you do anything
useful, it is better to pattern match in the function argument. Consider these examples:
Bad Good
fun f arg1 arg2 = let fun f (x,y) (z,_) = ...
val x = #1 arg1
val y = #2 arg1
val z = #1 arg2
in
...
end
Function Arguments Should Not Use Values for Patterns. You should only deconstruct values with variable
names and/or wildcards in function arguments. If you want to pattern match against a specific value, use a case
expression or an if expression. We include this rule because there are too many errors that can occur when you
don't do this exactly right. Thus of the following two examples, you should use the latter:
fun fact 0 = 1
| fact n = n * fact(n-1)
fun fact n =
if n=0 then 1
else n * fact(n-1)
Avoid Unnecessary Projections. Prefer pattern matching to projections with function arguments or a value
declarations. Using projections is okay as long as it is infrequent and the meaning is clearly understood from the
context. The above rule shows how to pattern-match in the function arguments. Here is an example for pattern
matching with value declarations.
Bad Good
let let
val v = someFunction() val (x,y) = someFunction()
val x = #1 v in
val y = #2 v x+y
in end
x+y
end
4 of 8 2/15/2021, 3:27 PM
CS312 SML Style Guide https://www.cs.cornell.edu/courses/cs312/2008sp/handouts/style.htm
Combine nested case Expressions. Rather than nest 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
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
Good
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
Avoid breaking expressions over multiple lines. If a tuple consists of more than two or three elements, you
should consider using a record instead of a tuple. Records have the advantage of placing each name on a separate
line and still looking good. Constructing a tuple over multiple lines makes for ugly code. Other expressions that take
up multiple lines should be done with a lot of thought. The best way to transform code that constructs expressions
over multiple lines to something that has good style is to factor the code using a let expression. Consider the
following:
Bad
fun euclid (m:int,n:int) : (int * int * int) =
if n=0
then (b 1, b 0, m)
else (#2 (euclid (n, m mod n)), u - (m div n) *
Better
fun euclid (m:int,n:int) : (int * int * int) =
if n=0
then (b 1, b 0, m)
else (#2 (euclid (n, m mod n)),
5 of 8 2/15/2021, 3:27 PM
CS312 SML Style Guide https://www.cs.cornell.edu/courses/cs312/2008sp/handouts/style.htm
Best
fun euclid (m:int,n:int) : (int * int * int) =
if n=0
then (b 1, b 0, m)
else let
val q = m div n
val r = n mod n
val (u,v,g) = euclid (n,r)
in
(v, u - q*v, g)
end
let
val x = TextIO.inputLine TextIO.stdIn
in
case x of
...
end
Good
Good
y*y + z
Verbosity
Don't Rewrite Library Functions. The basis library and the SML/NJ library have a great number of functions and
data structures -- use them! Often students will recode List.filter, List.map, and similar functions. A more
subtle situation for recoding is all the fold functions. Writing a function that recursively walks down the list should
make vigorous use of List.foldl or List.foldr. Other data structures often have a folding function; use them
whenever they are available.
Misusing if Expressions. Remember that the type of the condition in an if expression is bool. In general, the
type of an if expression is 'a, but in the case that the type is bool, you should not be using if at all. Consider the
following:
Bad Good
if e then true else false e
if e then false else true not e
if beta then beta else false beta
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
6 of 8 2/15/2021, 3:27 PM
CS312 SML Style Guide https://www.cs.cornell.edu/courses/cs312/2008sp/handouts/style.htm
never be used in place of an if expression (that's why if exists). Note the following:
case e of
true => x
| false => y
if e then x else y
The latter is much better. Another situation where if expressions are preferred over case expressions is as
follows:
case e of
c => x (* c is a constant value *)
| _ => y
The latter is definitely better. The other misuse is using case when pattern matching with a val declaration is
enough. Consider the following:
Bad Good
l::nil [l]
l::[] [l]
length + 0 length
length * 1 length
let val x = big exp in
big exp * same big exp
x*x end
if x then f a b c1 f a b if x then c1 else
else f a b c2 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
Don't Rewrap Functions. When passing a function as an argument to another function, don't rewrap the function
unnecessarily. Here's an example:
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:
7 of 8 2/15/2021, 3:27 PM
CS312 SML Style Guide https://www.cs.cornell.edu/courses/cs312/2008sp/handouts/style.htm
foldl (op +) 0
let
val x = 42
in
let
val y = x + 101
in
x + y
end
end
let
val x = 42
val y = x + 101
in
x + y
end
8 of 8 2/15/2021, 3:27 PM