Formatting Conventions
This document is a historical reference. It was the precursor to the Microsoft F# formatting style guide, which is now the authoritative source for F# formatting conventions. We preserve it here for its legacy value to the project.
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.
Table of Contents
- General rules for indentation
- Formatting rules for syntactic constructs
- Conclusions
- References
General rules for indentation
Using spaces
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.
Offside rule
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 the then keyword or the else keyword introduces an offside line.
- In a try...with expression, the first token after try introduces an offside line.
- In a match expression, the first token after with 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, and module introduce offside lines.
Formatting rules for syntactic constructs
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.
Type definitions
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
Value declarations
Tuples
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
Records
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.
Lists and arrays
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|]
|]
Discriminated unions
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)
)
Conditional expressions
Multiple branches
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
Single branches
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
Pattern matching constructs
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.
Function applications
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
Infix operators
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 operators
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
Modules
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
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
Whitespaces
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)
Blank lines
- 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.
2020 Revision
Blank lines are introduces around any multiline code constructs:
let a = 9
if someCondition then
printfn "meh"
()
let b = 10
let c = 10
The SynExpr.IfThenElse expression is multiline so a blank line between let a and if someCondition and between if someCondition and let b is fitting.
Single line statements are combined without any additional blank lines, see let b and let c.
Comments
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.
Conclusions
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.
References
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".
val float: value: 'T -> float (requires member op_Explicit)
--------------------
type float = System.Double
--------------------
type float<'Measure> = float
[<Struct>] type DateTime = new: date: DateOnly * time: TimeOnly -> unit + 16 overloads member Add: value: TimeSpan -> DateTime member AddDays: value: float -> DateTime member AddHours: value: float -> DateTime member AddMicroseconds: value: float -> DateTime member AddMilliseconds: value: float -> DateTime member AddMinutes: value: float -> DateTime member AddMonths: months: int -> DateTime member AddSeconds: value: float -> DateTime member AddTicks: value: int64 -> DateTime ...
<summary>Represents an instant in time, typically expressed as a date and time of day.</summary>
--------------------
System.DateTime ()
(+0 other overloads)
System.DateTime(ticks: int64) : System.DateTime
(+0 other overloads)
System.DateTime(date: System.DateOnly, time: System.TimeOnly) : System.DateTime
(+0 other overloads)
System.DateTime(ticks: int64, kind: System.DateTimeKind) : System.DateTime
(+0 other overloads)
System.DateTime(date: System.DateOnly, time: System.TimeOnly, kind: System.DateTimeKind) : System.DateTime
(+0 other overloads)
System.DateTime(year: int, month: int, day: int) : System.DateTime
(+0 other overloads)
System.DateTime(year: int, month: int, day: int, calendar: System.Globalization.Calendar) : System.DateTime
(+0 other overloads)
System.DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int) : System.DateTime
(+0 other overloads)
System.DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, kind: System.DateTimeKind) : System.DateTime
(+0 other overloads)
System.DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, calendar: System.Globalization.Calendar) : System.DateTime
(+0 other overloads)
<summary>Gets a <see cref="T:System.DateTime" /> object that is set to the current date and time on this computer, expressed as the local time.</summary>
<returns>An object whose value is the current local date and time.</returns>
<summary>Gets the seconds component of the date represented by this instance.</summary>
<returns>The seconds component, expressed as a value between 0 and 59.</returns>
type Exception = interface ISerializable new: unit -> unit + 2 overloads member GetBaseException: unit -> exn member GetObjectData: info: SerializationInfo * context: StreamingContext -> unit member GetType: unit -> Type member ToString: unit -> string member Data: IDictionary member HResult: int member HelpLink: string member InnerException: exn ...
<summary>Represents errors that occur during application execution.</summary>
--------------------
System.Exception() : System.Exception
System.Exception(message: string) : System.Exception
System.Exception(message: string, innerException: exn) : System.Exception
type ApplicationException = inherit exn new: unit -> unit + 2 overloads
<summary>Serves as the base class for application-defined exceptions.</summary>
--------------------
System.ApplicationException() : System.ApplicationException
System.ApplicationException(message: string) : System.ApplicationException
System.ApplicationException(message: string, innerException: exn) : System.ApplicationException
module List from Microsoft.FSharp.Collections
--------------------
type List<'T> = | op_Nil | op_ColonColon of Head: 'T * Tail: 'T list interface IReadOnlyList<'T> interface IReadOnlyCollection<'T> interface IEnumerable interface IEnumerable<'T> member GetReverseIndex: rank: int * offset: int -> int member GetSlice: startIndex: int option * endIndex: int option -> 'T list static member Cons: head: 'T * tail: 'T list -> 'T list member Head: 'T member IsEmpty: bool member Item: index: int -> 'T with get ...
<summary>Represents an application domain, which is an isolated environment where applications execute. This class cannot be inherited.</summary>
<summary>Gets the current application domain for the current <see cref="T:System.Threading.Thread" />.</summary>
<returns>The current application domain.</returns>
System.Type.GetMethods(bindingAttr: System.Reflection.BindingFlags) : System.Reflection.MethodInfo array
val string: value: 'T -> string
--------------------
type string = System.String
val int: value: 'T -> int (requires member op_Explicit)
--------------------
type int = int32
--------------------
type int<'Measure> = int
type StreamReader = inherit TextReader new: stream: Stream -> unit + 12 overloads member Close: unit -> unit member DiscardBufferedData: unit -> unit member Peek: unit -> int member Read: unit -> int + 2 overloads member ReadAsync: buffer: char array * index: int * count: int -> Task<int> + 1 overload member ReadBlock: buffer: char array * index: int * count: int -> int + 1 overload member ReadBlockAsync: buffer: char array * index: int * count: int -> Task<int> + 1 overload member ReadLine: unit -> string ...
<summary>Implements a <see cref="T:System.IO.TextReader" /> that reads characters from a byte stream in a particular encoding.</summary>
--------------------
System.IO.StreamReader(stream: System.IO.Stream) : System.IO.StreamReader
(+0 other overloads)
System.IO.StreamReader(path: string) : System.IO.StreamReader
(+0 other overloads)
System.IO.StreamReader(stream: System.IO.Stream, detectEncodingFromByteOrderMarks: bool) : System.IO.StreamReader
(+0 other overloads)
System.IO.StreamReader(stream: System.IO.Stream, encoding: System.Text.Encoding) : System.IO.StreamReader
(+0 other overloads)
System.IO.StreamReader(path: string, detectEncodingFromByteOrderMarks: bool) : System.IO.StreamReader
(+0 other overloads)
System.IO.StreamReader(path: string, options: System.IO.FileStreamOptions) : System.IO.StreamReader
(+0 other overloads)
System.IO.StreamReader(path: string, encoding: System.Text.Encoding) : System.IO.StreamReader
(+0 other overloads)
System.IO.StreamReader(stream: System.IO.Stream, encoding: System.Text.Encoding, detectEncodingFromByteOrderMarks: bool) : System.IO.StreamReader
(+0 other overloads)
System.IO.StreamReader(path: string, encoding: System.Text.Encoding, detectEncodingFromByteOrderMarks: bool) : System.IO.StreamReader
(+0 other overloads)
System.IO.StreamReader(stream: System.IO.Stream, encoding: System.Text.Encoding, detectEncodingFromByteOrderMarks: bool, bufferSize: int) : System.IO.StreamReader
(+0 other overloads)
fantomas