F# code formatting guidelines

This article offers guidelines for how to format your code so that your F# code is:

  • Generally viewed as more legible
  • Is in accordance with conventions applied by formatting tools in Visual Studio and other editors
  • Similar to other code online

These guidelines are based on A comprehensive guide to F# Formatting Conventions by Anh-Dung Phan.

General rules for indentation

F# uses significant white space by default. The following guidelines are intended to provide guidance as to how to juggle some challenges this can impose.

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.

We recommend 4 spaces per indentation.

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 and use it systematically throughout your codebase.

Formatting 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 (for example, a set of dummy implementations).
  • Use blank lines in functions, sparingly, to indicate logical sections.

Formatting comments

Generally prefer multiple double-slash comments over ML-style block comments.

// Prefer this style of comments when you want
// to express written ideas on multiple lines.

(*
    ML-style comments are fine, but not a .NET-ism.
    They are useful when needing to modify multi-line comments, though.
*)

Inline comments should capitalize the first letter.

let f x = x + 1 // Increment by one.

Naming conventions

Use camelCase for class-bound, expression-bound and pattern-bound values and functions

It is common and accepted F# style to use camelCase for all names bound as local variables or in pattern matches and function definitions.

// OK
let addIAndJ i j = i + j

// Bad
let addIAndJ I J = I+J

// Bad
let AddIAndJ i j = i + j

Locally-bound functions in classes should also use camelCase.

type MyClass() =

    let doSomething () =

    let firstResult = ...

    let secondResult = ...

    member x.Result = doSomething()

Use camelCase for module-bound public functions

When a module-bound function is part of a public API, it should use camelCase:

module MyAPI =
    let publicFunctionOne param1 param2 param2 = ...

    let publicFunctionTwo param1 param2 param3 = ...

Use camelCase for internal and private module-bound values and functions

Use camelCase for private module-bound values, including the following:

  • Ad hoc functions in scripts

  • Values making up the internal implementation of a module or type

let emailMyBossTheLatestResults =
    ...

Use camelCase for parameters

All parameters should use camelCase in accordance with .NET naming conventions.

module MyModule =
    let myFunction paramOne paramTwo = ...

type MyClass() =
    member this.MyMethod(paramOne, paramTwo) = ...

Use PascalCase for modules

All modules (top-level, internal, private, nested) should use PascalCase.

module MyTopLevelModule

module Helpers =
    module private SuperHelpers =
        ...

    ...

Use PascalCase for type declarations, members, and labels

Classes, interfaces, structs, enumerations, delegates, records, and discriminated unions should all be named with PascalCase. Members within types and labels for records and discriminated unions should also use PascalCase.

type IMyInterface =
    abstract Something: int

type MyClass() =
    member this.MyMethod(x, y) = x + y

type MyRecord = { IntVal: int; StringVal: string }

type SchoolPerson =
    | Professor
    | Student
    | Advisor
    | Administrator

Use PascalCase for constructs intrinsic to .NET

Namespaces, exceptions, events, and project/.dll names should also use PascalCase. Not only does this make consumption from other .NET languages feel more natural to consumers, it's also consistent with .NET naming conventions that you are likely to encounter.

Avoid underscores in names

Historically, some F# libraries have used underscores in names. However, this is no longer widely accepted, partly because it clashes with .NET naming conventions. That said, some F# programmers use underscores heavily, partly for historical reasons, and tolerance and respect is important. However, be aware that the style is often disliked by others who have a choice about whether to use it.

Some exceptions includes interoperating with native components, where underscores are very common.

Use standard F# operators

The following operators are defined in the F# standard library and should be used instead of defining equivalents. Using these operators is recommended as it tends to make code more readable and idiomatic. Developers with a background in OCaml or other functional programming language may be accustomed to different idioms. The following list summarizes the recommended F# operators.

x |> f // Forward pipeline
f >> g // Forward composition
x |> ignore // Discard away a value
x + y // Overloaded addition (including string concatenation)
x - y // Overloaded subtraction
x * y // Overloaded multiplication
x / y // Overloaded division
x % y // Overloaded modulus
x && y // Lazy/short-cut "and"
x || y // Lazy/short-cut "or"
x <<< y // Bitwise left shift
x >>> y // Bitwise right shift
x ||| y // Bitwise or, also for working with “flags” enumeration
x &&& y // Bitwise and, also for working with “flags” enumeration
x ^^^ y // Bitwise xor, also for working with “flags” enumeration

Use prefix syntax for generics (Foo<T>) in preference to postfix syntax (T Foo)

F# inherits both the postfix ML style of naming generic types (for example, int list) as well as the prefix .NET style (for example, list<int>). Prefer the .NET style, except for four specific types:

  1. For F# Lists, use the postfix form: int list rather than list<int>.
  2. For F# Options, use the postfix form: int option rather than option<int>.
  3. For F# arrays, use the syntactic name int[] rather than int array or array<int>.
  4. For Reference Cells, use int ref rather than ref<int> or Ref<int>.

For all other types, use the prefix form.

Formatting tuples

A tuple instantiation should be parenthesized, and the delimiting commas within should be followed by a single space, for example: (1, 2), (x, y, z).

It is commonly accepted to omit parentheses in pattern matching of tuples:

let (x, y) = z // Destructuring
let x, y = z // OK

// OK
match x, y with
| 1, _ -> 0
| x, 1 -> 0
| x, y -> 1

Formatting discriminated union declarations

Indent | in type definition by 4 spaces:

// OK
type Volume =
    | Liter of float
    | FluidOunce of float
    | ImperialPint of float

// Not OK
type Volume =
| Liter of float
| USPint of float
| ImperialPint of float

Formatting discriminated unions

Instantiated Discriminated Unions that split across multiple lines should give contained data a new scope with indentation:

let tree1 =
    BinaryNode
        (BinaryNode(BinaryValue 1, BinaryValue 2),
         BinaryNode(BinaryValue 3, BinaryValue 4))

The closing parenthesis can also be on a new line:

let tree1 =
    BinaryNode(
        BinaryNode(BinaryValue 1, BinaryValue 2),
        BinaryNode(BinaryValue 3, BinaryValue 4)
    )

Formatting record declarations

Indent { in type definition by 4 spaces and start the field list on the same line:

// OK
type PostalAddress =
    { Address: string
      City: string
      Zip: string }
    member x.ZipAndCity = sprintf "%s %s" x.Zip x.City

// Not OK
type PostalAddress =
  { Address: string
    City: string
    Zip: string }
    member x.ZipAndCity = sprintf "%s %s" x.Zip x.City

// Unusual in F#
type PostalAddress =
    { 
        Address: string
        City: string
        Zip: string
    }

Placing the opening token on the same line and the closing token on a new line is also fine, but be aware that you need to use the verbose syntax to define members (the with keyword):

//  OK, but verbose syntax required
type PostalAddress = { 
    Address: string
    City: string
    Zip: string
} with
    member x.ZipAndCity = sprintf "%s %s" x.Zip x.City

Formatting records

Short records can be written in one line:

let point = { X = 1.0; Y = 0.0 }

Records that are longer should use new lines for labels:

let rainbow =
    { Boss = "Jeffrey"
      Lackeys = ["Zippy"; "George"; "Bungle"] }

Placing the opening token on the same line and the closing token on a new line is also fine:

let rainbow = {
    Boss1 = "Jeffrey"
    Boss2 = "Jeffrey"
    Boss3 = "Jeffrey"
    Boss4 = "Jeffrey"
    Boss5 = "Jeffrey"
    Boss6 = "Jeffrey"
    Boss7 = "Jeffrey"
    Boss8 = "Jeffrey"
    Lackeys = ["Zippy"; "George"; "Bungle"]
}

The same rules apply for list and array elements.

Formatting 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 brace-like operators. For example, 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|]
|]

Formatting if expressions

Indentation of conditionals depends on the sizes of the expressions that make them up. If cond, e1 and e2 are short, simply write them on one line:

if cond then e1 else e2

If either cond, e1 or e2 are longer, but not multi-line:

if cond
then e1
else e2

If any of the expressions are multi-line:

if cond then
    e1
else
    e2

Multiple conditionals with elif and else are indented at the same scope as the if:

if cond1 then e1
elif cond2 then e2
elif cond3 then e3
else e4

Pattern matching constructs

Use a | for each clause of a match with no indentation. If the expression is short, you can consider using a single line if each subexpression is also simple.

// OK
match l with
| { him = x; her = "Posh" } :: tail -> _
| _ :: tail -> findDavid tail
| [] -> failwith "Couldn't find David"

// Not OK
match l with
    | { him = x; her = "Posh" } :: tail -> _
    | _ :: tail -> findDavid tail
    | [] -> failwith "Couldn't find David"

If the expression on the right of the pattern matching arrow is too large, move it to the following line, indented one step from the match/|.

match lam with
| Var v -> 1
| Abs(x, body) ->
    1 + sizeLambda body
| App(lam1, lam2) ->
    sizeLambda lam1 + sizeLambda lam2

Pattern matching of anonymous functions, starting by function, should generally not indent too far. For example, indenting one scope as follows is fine:

lambdaList
|> List.map (function
    | Abs(x, body) -> 1 + sizeLambda 0 body
    | App(lam1, lam2) -> sizeLambda (sizeLambda 0 lam1) lam2
    | Var v -> 1)

Pattern matching in functions defined by let or let rec should be indented 4 spaces after starting of let, even if function keyword is 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

We do not recommend aligning arrows.

Formatting try/with expressions

Pattern matching on the exception type should be indented at the same level as with.

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"

Formatting function parameter application

In general, most function parameter application is done on the same line.

If you wish to apply parameters to a function on a new line, indent them by one scope.

// OK
sprintf "\t%s - %i\n\r"
     x.IngredientName x.Quantity

// OK
sprintf
     "\t%s - %i\n\r"
     x.IngredientName x.Quantity

// OK
let printVolumes x =
    printf "Volume in liters = %f, in us pints = %f, in imperial = %f"
        (convertVolumeToLiter x)
        (convertVolumeUSPint x)
        (convertVolumeImperialPint x)

The same guidelines apply for lambda expressions as function arguments. If the body of a lambda expression, the body can have another line, indented by one scope

let printListWithOffset a list1 =
    List.iter
        (fun elem -> printfn "%d" (a + elem))
        list1

// OK if lambda body is long enough
let printListWithOffset a list1 =
    List.iter
        (fun elem ->
            printfn "%d" (a + elem))
        list1

However, if the body of a lambda expression is more than one line, consider factoring it out into a separate function rather than have a multi-line construct applied as a single argument to a function.

Formatting infix operators

Separate operators by spaces. Obvious exceptions to this rule are the ! and . operators.

Infix expressions are OK to lineup on same column:

acc +
(sprintf "\t%s - %i\n\r"
     x.IngredientName x.Quantity)

let function1 arg1 arg2 arg3 arg4 =
    arg1 + arg2 +
    arg3 + arg4

Formatting pipeline operators

Pipeline |> operators should go underneath the expressions they operate on.

// Preferred approach
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

Formatting modules

Code in a local module must be indented relative to the module, but code in a top-level module should not be indented. Namespace elements do not have to be indented.

// 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

Formatting object expressions and interfaces

Object expressions and interfaces should be aligned in the same way with member being indented after 4 spaces.

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
              reversed.CompareTo (rev s2) }

Formatting white space in expressions

Avoid extraneous white space in F# expressions.

// OK
spam (ham.[1])

// Not OK
spam ( ham.[ 1 ] )

Named arguments should also not have space surrounding the =:

// OK
let makeStreamReader x = new System.IO.StreamReader(path=x)

// Not OK
let makeStreamReader x = new System.IO.StreamReader(path = x)