G-Research / fsharp-formatting-conventions

G-Research F# code formatting guidelines
Apache License 2.0
19 stars 7 forks source link
gr-oss

G-Research F# code formatting guidelines

These guidelines are based on the official F# code formatting guidelines.

Table of contents

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.

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 white space

F# is white space sensitive. Although most semantics from white space are covered by proper indentation, there are some other things to consider.

Formatting operators in arithmetic expressions

Always use white space around binary arithmetic expressions:

let subtractThenAdd x = x - 1 + 3

Unary - operators should always have the value they are negating immediately follow:

// OK
let negate x = -x

// Bad
let negateBad x = - x

Adding a white-space character after the - operator can lead to confusion for others.

In summary, it's important to always:

The binary arithmetic operator guideline is especially important. Failing to surround a binary - operator, when combined with certain formatting choices, could lead to interpreting it as a unary -.

Surround a custom operator definition with white space

Always use white space to surround an operator definition:

// OK
let ( !> ) x f = f x

// Bad
let (!>) x f = f x

For any custom operator that starts with * and that has more than one character, you need to add a white space to the beginning of the definition to avoid a compiler ambiguity. Because of this, we recommend that you simply surround the definitions of all operators with a single white-space character.

Surround function parameter arrows with white space

When defining the signature of a function, use white space around the -> symbol:

// OK
type MyFun = int -> int -> string

// Bad
type MyFunBad = int->int->string

Surround function arguments with white space

When defining a function, use white space around each argument.

// OK
let myFun (a : decimal) b c = a + b + c

// Bad
let myFunBad (a:decimal)(b)c = a + b + c

Place parameters on a new line for very long member definitions

If you have a very long member definition, place the parameters on new lines and indent them one scope.

type C () =
    member __.LongMethodWithLotsOfParameters
        (
            aVeryLongType : AVeryLongTypeThatYouNeedToUse,
            aSecondVeryLongType : AVeryLongTypeThatYouNeedToUse,
            aThirdVeryLongType : AVeryLongTypeThatYouNeedToUse
        )
        =
        // ... the body of the method follows

This also applies to constructors:

type C 
    (
        aVeryLongType : AVeryLongTypeThatYouNeedToUse,
        aSecondVeryLongType : AVeryLongTypeThatYouNeedToUse,
        aThirdVeryLongType : AVeryLongTypeThatYouNeedToUse
    )
    =
    // ... the body of the class follows

Place parameters on a new line for member declarations

If a member declaration is too long to fit on one line, split it up into one member per line.

type Foo =
    abstract Qux :
        string
        -> string
        -> int

type Bar =
    abstract Qux :
        input : string
        -> modifier : string
        -> int

type Baz =
    abstract Qux :
        [<Attribute>] input : string
        -> [<Attribute>] modifier : string
        -> int

In the same way, format tuples over new lines if the declaration is too long to fit on one line:

type Foo =
    abstract Qux :
        string
        * string
        -> int

type Bar =
    abstract Qux :
        input : string
        * modifier : string
        -> int

type Baz =
    abstract Qux :
        [<Attribute>] input : string
        * [<Attribute>] modifier : string
        -> int

type Both =
    /// int -> (string * int * string) -> string
    abstract Qux :
        i : int ->
        a : string
        * foo : int
        * otherInput : string ->
        string

Type annotations

Pad function argument type annotations

When defining arguments with type annotations, use white space before and after the : symbol:

// OK
let complexFunction (a : int) (b : int) c = a + b + c

// Bad
let complexFunctionBad (a :int) (b: int) (c:int) = a + b + c

Surround return type annotations with white space

In a let-bound function or value type annotation (return type in the case of a function), use white space before and after the : symbol:

// OK
let expensiveToCompute : int = 0 // Type annotation for let-bound value
let myFun (a : decimal) b c : decimal = a + b + c // Type annotation for the return type of a function
// Bad
let expensiveToComputeBad1:int = 1
let expensiveToComputeBad2 :int = 2
let myFunBad (a: decimal) b c:decimal = a + b + c

Long function signatures

If you have a very long function signature, place the parameters on new lines and indent them one scope. The return type and equal sign are also placed on their own line.

let longFunctionName
    (aVeryLongFunctionParameter : int)
    (anotherVeryLongFunctionParameter : int)
    : int
    =
    // ... the body of the function
let anotherLongFunction
    someParameter
    anotherParameter
    =
    // ... the body of the function

Formatting blank lines

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.
*)

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:

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.

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>). Which style to use depends on local readability, but in particular:

  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# Value Options, use the postfix form: int voption rather than voption<int>.
  4. For F# arrays, use the syntactic name int[] or int array rather than array<int>.
  5. For Reference Cells, use int ref rather than ref<int> or Ref<int>.

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

It is also commonly accepted to omit parentheses if the tuple is the return value of a function:

// OK
let update model msg =
    match msg with
    | 1 -> model + 1, []
    | _ -> model, [ msg ]

In summary, prefer parenthesized tuple instantiations, but when using tuples for pattern matching or a return value, it is considered fine to avoid parentheses.

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

The F# spec does allow you to omit the pipe (|) character in a single-case DU. Nevertheless, prefer for consistency to use the pipe even when there is only one case.

// Preferred
type Foo =
    | Foo of int

type Foo = | Foo of int

// Avoid
type Foo = Foo of int

Formatting discriminated unions

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

Closing parenthesis should 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 next line:

type PostalAddress =
    {
        Address : string
        City : string
        Zip : string
    }

And if you are declaring interface implementations or members on the record, note that with should not be used:

// Declaring additional members on PostalAddress
type PostalAddress =
    {
        Address : string
        City : string
        Zip : string
    }

    member x.ZipAndCity = sprintf "%s %s" x.Zip x.City

type MyRecord =
    {
        SomeField : int
    }
    interface IMyInterface

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 a new line, the contents tabbed over one scope, and the closing token on a new line is preferable.

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

type MyRecord =
    {
        SomeField : int
    }
    interface IMyInterface

let foo a =
    a
    |> Option.map (fun x ->
        {
            MyField = x
        }
    )

The same rules apply for list and array elements.

Insert a space before and after semicolons:

let good = { X = 1.0 ; Y = 0.0 }
let bad = { X = 1.0; Y = 0.0 }

Formatting copy-and-update record expressions

A copy-and-update record expression is still a record, so similar guidelines apply.

Short expressions can fit on one line:

let point2 = { point with X = 1 ; Y = 2 }

Longer expressions should use new lines:

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

And as with the record guidance, you may want to dedicate separate lines for the braces and indent one scope to the right with the expression.

type S = { F1 : int ; F2 : string }
type State = { F : S option }

let state = { F = Some { F1 = 1 ; F2 = "Hello" } }
let newState =
    { state with
        F =
            Some
                {
                    F1 = 0
                    F2 = ""
                }
    }

Formatting lists and arrays

Write x :: l with spaces around the :: operator (:: is an infix operator, hence surrounded by spaces).

List and arrays declared on a single line should have a space after the opening bracket and before the closing bracket:

let xs = [ 1 ; 2 ; 3 ]
let ys = [| 1 ; 2 ; 3 ; |]

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 } ]

// Better
[
    { 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 }]

The same guideline applies for lists or arrays of tuples.

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

And as with records, declaring the opening and closing brackets on their own line will make moving code around and piping into functions easier.

When generating arrays and lists programmatically, do...yield may aid in readability. These cases, though subjective, should be taken into consideration.

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 any of the expressions are longer:

if cond then
    e1
else
    e2

For nested conditionals, else if may appear either on a new line or on the old line:

if cond then
    e1
elif cond2 then // Prefer `elif` to `else if`
    e2
else
    if foo then 3 // also OK
    else
        someMoreStuff
if cond then
    e1
else if cond2 then // OK, but this could have been `elif`
    e2
else
    if foo then 3 // also OK
    else
        someMoreStuff

Where it would make code more readable, the final block of an else or a match may be unindented. This is to allow flows which perform several checks in sequence, earlying out with an Error result if any check fails, without the code marching off to the right.

if cond then
    e1
elif cond2 then
    e2
else

if foo then 3 // note the acceptable unindentation of this line
else
    someMoreStuff // this line could be unindented too

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

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
)

Note that the closing bracket has appeared on a new line, indented to the scope of the pipe.

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 (System.Exception ())
    else
        raise (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.

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

// 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 to require splitting lines
let printListWithOffset a list1 =
    List.iter
        (fun elem ->
            printfn "%d" (a + elem)
        )
        list1

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

// OK if lambda body is long enough to require splitting...
let printListWithOffset a list1 =
    list1
    |> List.iter (
        ((+) veryVeryVeryVeryLongThing)
        >> printfn "%d"
    )

// ... but if lambda body will fit on one line, don't split
let printListWithOffset' a list1 =
    list1
    |> List.iter (((+) a) >> printfn "%d")

// If any argument will not fit on a line, split all the arguments onto different lines
let mySuperFunction v =
    someOtherFunction
        (fun a ->
            let meh = "foo"
            a
        )
        somethingElse
        (fun b -> 42)
        v

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.

The following two functions are both correctly formatted (the function is aligned analogously to the match).

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

myList
|> List.map (fun i ->
    match i with
    | Abs (x, body) -> 1 + sizeLambda 0 body
    | App (lam1, lam2) -> sizeLambda (sizeLambda 0 lam1) lam2
    | Var v -> 1
)

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 module must be indented relative to the module. 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. Also avoid extraneous brackets.

// OK
spam ham.[1]

// OK
spam (ham.[1], 3)

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

Formatting attributes

Attributes are placed above a construct:

[<SomeAttribute>]
type MyClass () = ...

[<RequireQualifiedAccess>]
module M =
    let f x = x

[<Struct>]
type MyRecord =
    {
        Label1 : int
        Label2 : string
    }

Formatting attributes on parameters

Attributes can also be places on parameters. In this case, place then on the same line as the parameter and before the name:

// Defines a class that takes an optional value as input defaulting to false.
type C () =
    member __.M([<Optional; DefaultParameterValue(false)>] doSomething : bool)

Formatting multiple attributes

When multiple attributes are applied to a construct that is not a parameter, they should be placed such that there is one attribute per line:

[<Struct>]
[<IsByRefLike>]
type MyRecord =
    {
        Label1 : int
        Label2 : string
    }

When applied to a parameter, they must be on the same line and separated by a ; separator.

Formatting literals

F# literals using the Literal attribute should place the attribute on its own line and use PascalCase naming:

[<Literal>]
let Path = __SOURCE_DIRECTORY__ + "/" + __SOURCE_FILE__

[<Literal>]
let MyUrl = "www.mywebsitethatiamworkingwith.com"

Avoid placing the attribute on the same line as the value.