This article is written mostly based on "F# Coding Guidelines" (offline version) from Don Syme. There are certain bits of the original document that need to be updated when F# has changed a lot in last few years. Therefore, I attempt to reintroduce F# Formatting Conventions here and add some relevant information from other sources as well. Another purpose of the article is to recognize requirements for an F# source code formatter I would like to create.
- General rules for indentation
- Formatting rules for syntactic constructs
- Conclusions
- References
When indentation is required, you must use spaces, not tabs. At least one space is required. Your organization can create coding standards to specify the number of spaces to use for indentation; two, three or four spaces of indentation at each level where indentation occurs is typical. That said, indentation of programs is a subjective matter. Variations are OK, but the first rule you should follow is consistency of indentation:
Choose a generally accepted style of indentation, then use it systematically throughout the whole application.
You can configure Visual Studio to match your organization's indentation standards by changing the options in the Options dialog box, which is available from the Tools menu. In the Text Editor node, expand F# and then click Tabs. For a description of the available options, see Options, Text Editor, All Languages, Tabs.
In general, when the compiler parses your code, it maintains an internal stack that indicates the current level of nesting.
When code is indented, a new level of nesting is created, or pushed onto this internal stack.
When a construct ends, the level is popped.
Indentation is one way to signal the end of a level and pop the internal stack, but certain tokens also cause the level to be popped, such as the end
keyword, or a closing brace or parenthesis.
A page is often 80 columns wide.
Code in a multiline construct, such as a type definition, function definition, try...with
construct, and looping constructs, must be indented relative to the opening line of the construct.
The first indented line establishes a column position for subsequent code in the same construct.
The indentation level is called a context. The column position sets a minimum column, referred to as an offside line, for subsequent lines of code that are in the same context.
When a line of code is encountered that is indented less than this established column position, the compiler assumes that the context has ended and that you are now coding at the next level up, in the previous context.
The term offside is used to describe the condition in which a line of code triggers the end of a construct because it is not indented far enough.
In other words, code to the left of an offside line is offside.
In correctly indented code, you take advantage of the offside rule in order to delineate the end of constructs.
If you use indentation improperly, an offside condition can cause the compiler to issue a warning or can lead to the incorrect interpretation of your code.
Offside lines are determined as follows.
- An
=
token associated with a let introduces an offside line at the column of the first token after the=
sign. - In an
if...then...else
expression, the column position of the first token after thethen
keyword or theelse
keyword introduces an offside line. - In a
try...with
expression, the first token aftertry
introduces an offside line. - In a
match
expression, the first token afterwith
and the first token after each->
introduce offside lines. - The first token after
with
in a type extension introduces an offside line. - The first token after an opening brace or parenthesis, or after the
begin
keyword, introduces an offside line. - The first character in the keywords
let
,if
, andmodule
introduce offside lines.
Keep in mind that code is read much more often than it is written. This section introduces a set of recommendations to improve the readability of code. Consistency with the recommendations is important. However, sometimes these formatting conventions do not apply. It is a good reason to break a particular rule, if applying it would make the code less readable.
In this section, code fragments without comments are of good styles. Bad coding styles will be explicitly specified by corresponding comments. Although I also use 4 spaces as the indentation standard, all the rules are equally applied for 2, 3 spaces, etc.
Indent |
in type definition by 4 spaces:
// OK
type Volume =
| Liter of float
| USPint of float
| ImperialPint of float
// Not OK
type Volume =
| Liter of float
| USPint of float
| ImperialPint of float
A tuple is parenthesized and the commas therein (delimiters) are each followed by a space e.g. (1, 2)
, (x, y, z)
.
A commonly accepted exception is to omit parentheses in pattern matching of tuples. The justification is to match multiple values, not construct new tuples.
let x, y = z
match x, y with
| 1, _ -> 0
| x, 1 -> 0
| x, y -> 1
Short records can be written in one line:
let point = { X = 1.0; Y = 0.0 }
Opening token for records starts in a new line. Closing token is normally on the end of line of last construct:
let rainbow =
{ boss = "Jeffrey"
lackeys = ["Zippy"; "George"; "Bungle"] }
Not everyone likes this style, and variation is ok. For large constructs (> 6 lines) the closing token can be on a fresh line:
let rainbow =
{ boss1 = "Jeffrey"
boss2 = "Jeffrey"
boss3 = "Jeffrey"
boss4 = "Jeffrey"
boss5 = "Jeffrey"
boss6 = "Jeffrey"
boss7 = "Jeffrey"
boss8 = "Jeffrey"
lackeys = ["Zippy"; "George"; "Bungle"]
}
Assume that all fields are aligned at the same column, the trailing ;
right before each line break is optional.
You can also optionally include a trailing ;
for the last entry.
The same rule applies for list and array elements.
Write x :: l
with spaces around the ::
operator (::
is an infix operator, hence surrounded by spaces) and [1; 2; 3]
(;
is a delimiter, hence followed by a space).
Always use at least one space between two distinct parenthetical operators (e.g. leave a space between a [
and a {
).
// OK
[ { IngredientName = "Green beans"; Quantity = 250 }
{ IngredientName = "Pine nuts"; Quantity = 250 }
{ IngredientName = "Feta cheese"; Quantity = 250 }
{ IngredientName = "Olive oil"; Quantity = 10 }
{ IngredientName = "Lemon"; Quantity = 1 } ]
// Not OK
[{ IngredientName = "Green beans"; Quantity = 250 }
{ IngredientName = "Pine nuts"; Quantity = 250 }
{ IngredientName = "Feta cheese"; Quantity = 250 }
{ IngredientName = "Olive oil"; Quantity = 10 }
{ IngredientName = "Lemon"; Quantity = 1 }]
Lists and arrays that split across multiple lines follow a similar rule as records do:
let pascalsTriangle =
[| [|1|]
[|1; 1|]
[|1; 2; 1|]
[|1; 3; 3; 1|]
[|1; 4; 6; 4; 1|]
[|1; 5; 10; 10; 5; 1|]
[|1; 6; 15; 20; 15; 6; 1|]
[|1; 7; 21; 35; 35; 21; 7; 1|]
[|1; 8; 28; 56; 70; 56; 28; 8; 1|]
|]
DUs that split across multiple lines follow a similar rule:
let tree1 =
BinaryNode
(BinaryNode(BinaryValue 1, BinaryValue 2),
BinaryNode(BinaryValue 3, BinaryValue 4))
However, the following way is also acceptable:
let tree1 =
BinaryNode(
BinaryNode(BinaryValue 1, BinaryValue 2),
BinaryNode(BinaryValue 3, BinaryValue 4)
)
Multiple conditionals open each line counting from the second one by the keyword else
or elif
:
if cond1 then e1
elif cond2 then e2
elif cond3 then e3
else e4
Indentation of conditionals depends on the sizes of the expressions which make them up.
If cond
, e1
and e2
are small, simply write them on one line:
if cond then e1 else e2
If e1
and cond
are small, but e2
is large:
if cond then e1
else
e2
If e1
and cond
are large and e2
is small:
if cond then
e1
else e2
If all the expressions are large:
if cond then
e1
else
e2
Rules of a with
in a try/with
can be optionally 4-space indented e.g.
try
if System.DateTime.Now.Second % 3 = 0 then
raise (new System.Exception())
else
raise (new System.ApplicationException())
with
| :? System.ApplicationException ->
printfn "A second that was not a multiple of 3"
| _ ->
printfn "A second that was a multiple of 3"
but this is also OK:
try
if System.DateTime.Now.Second % 3 = 0 then
raise (new System.Exception())
else
raise (new System.ApplicationException())
with
| :? System.ApplicationException ->
printfn "A second that was not a multiple of 3"
| _ ->
printfn "A second that was a multiple of 3"
Use a |
for each clause of a match (strictly speaking it is optional for the first), except when the match is all on one line.
// OK
match l with
| { him = x; her = "Posh" } :: tail -> x
| _ :: tail -> findDavid tail
| [] -> failwith "Couldn't find David"
// Not OK
match l with
| { him = x; her = "Posh" } :: tail -> x
| _ :: tail -> findDavid tail
| [] -> failwith "Couldn't find David"
// OK
match l with [] -> false | _ :: _ -> true
If the expression on the right of the pattern matching arrow is too large, cut the line after the arrow.
match lam with
| Abs(x, body) ->
1 + sizeLambda body
| App(lam1, lam2) ->
sizeLambda lam1 + sizeLambda lam2
| Var v -> 1
Some programmers apply this rule systematically to any clause of any pattern matching. This does not add any good to readability hence is not recommended.
// Not OK
let rec fib = function
| 0 ->
1
| 1 ->
1
| n ->
fib (n - 1) + fib (n - 2)
Pattern matching of anonymous functions, starting by function
, are indented with respect to the function
keyword:
List.map (function
| Abs(x, body) -> 1 + sizeLambda 0 body
| App(lam1, lam2) -> sizeLambda (sizeLambda 0 lam1) lam2
| Var v -> 1) lambdaList
Pattern matching in functions defined by let
or let rec
are indented 4 spaces after starting of let
although function
keyword may be used:
let rec sizeLambda acc = function
| Abs(x, body) -> sizeLambda (succ acc) body
| App(lam1, lam2) -> sizeLambda (sizeLambda acc lam1) lam2
| Var v -> succ acc
Careful alignment of the arrows of a pattern matching is considered bad practice, as exemplify in the following fragment:
// Not OK
let f = function
| C1 -> 1
| LongName _ -> 2
| _ -> 3
The justification is that it is harder to maintain the program. Adding a new case may screw up indentation and we often give up alignment at that time.
Arguments are always indented from functions:
// OK
Printf.sprintf "\t%s - %i\n\r"
x.IngredientName x.Quantity
// OK
Printf.sprintf
"\t%s - %i\n\r"
x.IngredientName x.Quantity
// OK
let printVolumes x =
Printf.printf "Volume in liters = %f, in us pints = %f, in imperial = %f"
(convertVolumeToLiter x)
(convertVolumeUSPint x)
(convertVolumeImperialPint x)
// Not OK
let printVolumes x =
Printf.printf "Volume in liters = %f, in us pints = %f, in imperial = %f"
(convertVolumeToLiter x)
(convertVolumeUSPint x)
(convertVolumeImperialPint x)
// Not OK
Printf.sprintf "\t%s - %i\n\r"
x.IngredientName x.Quantity
Anonymous function arguments can be either on next line or with a dangling fun
on the argument line:
// OK
let printListWithOffset a list1 =
List.iter (fun elem ->
printfn "%d" (a + elem)) list1
// Tolerable
let printListWithOffset a list1 =
List.iter (
fun elem ->
printfn "%d" (a + elem)) list1
Be careful to keep operator symbols well separated by spaces; not only will your formulas be more readable, but you will avoid confusion with multi-character operators.
Obvious exceptions to this rule are the !
and .
symbols.
They are not separated from their arguments.
Moreover, infix expressions are OK to lineup on same column:
acc +
(Printf.sprintf "\t%s - %i\n\r"
x.IngredientName x.Quantity)
let function1 arg1 arg2 arg3 arg4 =
arg1 + arg2
+ arg3 + arg4
Pipeline |>
should go at the start of a line immediately under the expression being operated on:
// OK
let methods2 =
System.AppDomain.CurrentDomain.GetAssemblies()
|> List.ofArray
|> List.map (fun assm -> assm.GetTypes())
|> Array.concat
|> List.ofArray
|> List.map (fun t -> t.GetMethods())
|> Array.concat
// OK
let methods2 = System.AppDomain.CurrentDomain.GetAssemblies()
|> List.ofArray
|> List.map (fun assm -> assm.GetTypes())
|> Array.concat
|> List.ofArray
|> List.map (fun t -> t.GetMethods())
|> Array.concat
// Not OK
let methods2 = System.AppDomain.CurrentDomain.GetAssemblies()
|> List.ofArray
|> List.map (fun assm -> assm.GetTypes())
|> Array.concat
|> List.ofArray
|> List.map (fun t -> t.GetMethods())
|> Array.concat
Code in a local module must be indented relative to the module, but code in a top-level module does not have to be indented. Namespace elements do not have to be indented. The following code examples illustrate this.
// A is a top-level module.
module A
let function1 a b = a - b * b
// A1 and A2 are local modules.
module A1 =
let function1 a b = a*a + b*b
module A2 =
let function2 a b = a*a - b*b
Object expressions and interfaces are aligned in the same way with member
being indented after 4 spaces.
For example, this is recommended:
let comparer =
{ new IComparer<string> with
member x.Compare(s1, s2) =
let rev (s : String) =
new String (Array.rev (s.ToCharArray()))
let reversed = rev s1 i
reversed.CompareTo (rev s2) }
but this is not advocated:
// Not OK
let comparer =
{ new IComparer<string> with
member x.Compare(s1, s2) =
let rev (s : String) =
new String (Array.rev (s.ToCharArray())) in
let reversed = rev s1 in
reversed.CompareTo (rev s2) }
Bodies of modules, classes, interfaces, and structures delimited by begin...end
, {...}
, class...end
, or interface...end
.
This allows for a style in which the opening keyword of a type definition can be on the same line as the type name without forcing the whole body to be indented further than the opening keyword.
type IMyInterface = interface
abstract Function1 : int -> int
end
Avoid extraneous whitespace in the following situations:
-
Immediately inside parentheses and brackets.
// OK spam (ham.[1]) // Not OK spam ( ham.[ 1 ] )
-
Immediately before a comma and semicolon.
-
Around the
=
sign when used to indicate a named argument.// OK let makeStreamReader x = new System.IO.StreamReader(path=x) // Not OK let makeStreamReader x = new System.IO.StreamReader(path = x)
- Separate top-level function and class definitions with two blank lines.
- Method definitions inside a class are separated by a single blank line.
- Extra blank lines may be used (sparingly) to separate groups of related functions. Blank lines may be omitted between a bunch of related one-liners (e.g. a set of dummy implementations).
- Use blank lines in functions, sparingly, to indicate logical sections.
Block comments generally apply to some (or all) code that follows them, and are indented to the same level as that code.
Each line of a block comment starts with a (*
or //
and a single space (unless it is indented text inside the comment).
Paragraphs inside a block comment are separated by a line containing a single *
or //
.
Use inline comments sparingly.
An inline comment is a comment on the same line as a statement.
Inline comments should be separated by at least two spaces from the statement.
They should start with a //
and a single space.
This guideline is still far from complete. Many syntactic constructs have not had any defined rule yet. In those cases, please keep in mind consistency of indentation rule and extrapolate from rules of similar constructs. Although I tried to keep a neutral position, there are mistakes and inconsistencies here and there. Feedbacks and suggestions for improving the article are always welcome.
This document is structured upon "F# Coding Guidelines" (offline version). General rules for indentation are referenced at "Code Formatting Guidelines (F#)". A few conventions for syntactic constructs are adapted from "Caml Programming Guidelines". Other whitespace-significant rules are taken from "PEP 8 -- Style Guide for Python Code".