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
Naming and declarations
Indenting
Parentheses
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
|