F# 代码格式设置准则

本文提供了有关如何设置代码格式的指南,以便 F# 代码:

  • 更易于识别
  • 遵循 Visual Studio Code 和其他编辑器中的格式设置工具应用的约定
  • 与网上其他代码类似

另请参阅编码约定组件设计指南,其中还包括命名约定。

自动代码格式设置

Fantomas 代码格式化程序是用于实现自动代码格式设置的 F# 社区标准工具。 默认设置对应于此样式指南。

我们强烈建议使用此代码格式化程序。 在 F# 团队中,代码格式设置规范应根据签入团队存储库的代码格式化程序的约定设置文件达成一致和编纂。

格式设置的一般规则

默认情况下,F# 使用有效空格,并且对空格敏感。 下面的指南旨在针对如何应对这可能带来的一些挑战提供指导。

使用空格而不是制表符

如果需要缩进,则必须使用空格,而不是制表符。 F# 代码不使用制表符,如果在字符串字面量或注释之外遇到制表符字符,则编译器将会给出错误。

使用一致的缩进

缩进时,至少需要一个空格。 组织可以创建编码标准来指定用于缩进的空格数;典型的做法是在缩进出现的各个级别使用两个、三个或四个空格进行缩进。

我们建议每个缩进使用四个空格。

也就是说,程序的缩进是一个主观问题。 可以有变化,但应遵循的第一条规则是缩进的一致性。 选择一种普遍接受的缩进样式,并在整个代码库中系统地使用它。

避免设置为对名称长度敏感的格式

尽量避免对命名敏感的缩进和对齐:

// ✔️ OK
let myLongValueName =
    someExpression
    |> anotherExpression

// ❌ Not OK
let myLongValueName = someExpression
                      |> anotherExpression

// ✔️ OK
let myOtherVeryLongValueName =
    match
        someVeryLongExpressionWithManyParameters
            parameter1
            parameter2
            parameter3
        with
    | Some _ -> ()
    | ...

// ❌ Not OK
let myOtherVeryLongValueName =
    match someVeryLongExpressionWithManyParameters parameter1
                                                   parameter2
                                                   parameter3 with
    | Some _ -> ()
    | ...

// ❌ Still Not OK
let myOtherVeryLongValueName =
    match someVeryLongExpressionWithManyParameters
              parameter1
              parameter2
              parameter3 with
    | Some _ -> ()
    | ...

避免此格式设置的主要原因是:

  • 重要代码会移到最右边
  • 留给实际代码的宽度较少
  • 重命名可能会破坏对齐

避免多余的空格

避免在 F# 代码中使用多余的空格(此样式指南中所述的情况除外)。

// ✔️ OK
spam (ham 1)

// ❌ Not OK
spam ( ham 1 )

设置注释格式

优先使用多个双斜杠注释而不是块注释。

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

(*
    Block comments can be used, but use sparingly.
    They are useful when eliding code sections.
*)

注释的第一个字母应该大写,并且是格式标准的短语或句子。

// ✔️ A good comment.
let f x = x + 1 // Increment by one.

// ❌ two poor comments
let f x = x + 1 // plus one

有关设置 XML 文档注释格式的信息,请参阅下面的“格式设置声明”。

设置表达式格式

本部分介绍如何设置不同类型的表达式的格式。

设置字符串表达式格式

字符串字面量和内插字符串都可以只保留在一行中,无论该行有多长。

let serviceStorageConnection =
    $"DefaultEndpointsProtocol=https;AccountName=%s{serviceStorageAccount.Name};AccountKey=%s{serviceStorageAccountKey.Value}"

不鼓励使用多行内插表达式, 而是将表达式结果绑定到一个值,并在内插字符串中使用该值。

设置元组表达式格式

元组实例化应该用圆括号括起来,其中的分隔逗号后面应该跟一个空格,例如:(1, 2)(x, y, z)

// ✔️ OK
let pair = (1, 2)
let triples = [ (1, 2, 3); (11, 12, 13) ]

在元组的模式匹配中,通常可以省略括号:

// ✔️ OK
let (x, y) = z
let x, y = z

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

如果元组是函数的返回值,通常也可以省略括号:

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

总之,最好使用带圆括号的元组实例化,但是当使用模式匹配或返回值的元组时,最好避免使用括号。

设置应用程序表达式格式

设置函数或方法应用程序的格式时,如果行宽允许,则在同一行上提供参数:

// ✔️ OK
someFunction1 x.IngredientName x.Quantity

除非参数需要括号,否则请省略括号:

// ✔️ OK
someFunction1 x.IngredientName

// ❌ Not preferred - parentheses should be omitted unless required
someFunction1 (x.IngredientName)

// ✔️ OK - parentheses are required
someFunction1 (convertVolumeToLiter x)

使用多个扩充参数进行调用时不要省略空格:

// ✔️ OK
someFunction1 (convertVolumeToLiter x) (convertVolumeUSPint x)
someFunction2 (convertVolumeToLiter y) y
someFunction3 z (convertVolumeUSPint z)

// ❌ Not preferred - spaces should not be omitted between arguments
someFunction1(convertVolumeToLiter x)(convertVolumeUSPint x)
someFunction2(convertVolumeToLiter y) y
someFunction3 z(convertVolumeUSPint z)

在默认格式设置约定中,将小写函数应用于元组或带圆括号的参数(即使使用单个参数)时会添加一个空格:

// ✔️ OK
someFunction2 ()

// ✔️ OK
someFunction3 (x.Quantity1 + x.Quantity2)

// ❌ Not OK, formatting tools will add the extra space by default
someFunction2()

// ❌ Not OK, formatting tools will add the extra space by default
someFunction3(x.IngredientName, x.Quantity)

在默认格式设置约定中,将大写方法应用于元组参数时不添加空格。 这是因为它们通常与 fluent programming 一起使用:

// ✔️ OK - Methods accepting parenthesize arguments are applied without a space
SomeClass.Invoke()

// ✔️ OK - Methods accepting tuples are applied without a space
String.Format(x.IngredientName, x.Quantity)

// ❌ Not OK, formatting tools will remove the extra space by default
SomeClass.Invoke ()

// ❌ Not OK, formatting tools will remove the extra space by default
String.Format (x.IngredientName, x.Quantity)

你可能需要在新行上将参数传递给函数,出于可读性考虑或因为参数列表或参数名称太长。 在这种情况下,缩进一级:

// ✔️ OK
someFunction2
    x.IngredientName x.Quantity

// ✔️ OK
someFunction3
    x.IngredientName1 x.Quantity2
    x.IngredientName2 x.Quantity2

// ✔️ OK
someFunction4
    x.IngredientName1
    x.Quantity2
    x.IngredientName2
    x.Quantity2

// ✔️ OK
someFunction5
    (convertVolumeToLiter x)
    (convertVolumeUSPint x)
    (convertVolumeImperialPint x)

当函数采用单个多行元组参数时,将每个参数放在一个新行中:

// ✔️ OK
someTupledFunction (
    478815516,
    "A very long string making all of this multi-line",
    1515,
    false
)

// OK, but formatting tools will reformat to the above
someTupledFunction
    (478815516,
     "A very long string making all of this multi-line",
     1515,
     false)

如果参数表达式很短,请用空格分隔参数并将其放在一行中。

// ✔️ OK
let person = new Person(a1, a2)

// ✔️ OK
let myRegexMatch = Regex.Match(input, regex)

// ✔️ OK
let untypedRes = checker.ParseFile(file, source, opts)

如果参数表达式很长,请使用换行符并缩进一级,而不是缩进到左括号。

// ✔️ OK
let person =
    new Person(
        argument1,
        argument2
    )

// ✔️ OK
let myRegexMatch =
    Regex.Match(
        "my longer input string with some interesting content in it",
        "myRegexPattern"
    )

// ✔️ OK
let untypedRes =
    checker.ParseFile(
        fileName,
        sourceText,
        parsingOptionsWithDefines
    )

// ❌ Not OK, formatting tools will reformat to the above
let person =
    new Person(argument1,
               argument2)

// ❌ Not OK, formatting tools will reformat to the above
let untypedRes =
    checker.ParseFile(fileName,
                      sourceText,
                      parsingOptionsWithDefines)

即使只有一个多行参数(包括多行字符串),同样的规则也适用:

// ✔️ OK
let poemBuilder = StringBuilder()
poemBuilder.AppendLine(
    """
The last train is nearly due
The Underground is closing soon
And in the dark, deserted station
Restless in anticipation
A man waits in the shadows
    """
)

Option.traverse(
    create
    >> Result.setError [ invalidHeader "Content-Checksum" ]
)

设置管道表达式格式

使用多行时,管道 |> 运算符应位于它们所操作的表达式下方。

// ✔️ 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, add a line break after "=" and put multi-line pipelines on multiple lines.
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 either
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

设置 Lambda 表达式格式

当 Lambda 表达式用作多行表达式中的参数并且后跟其他参数时,将 Lambda 表达式的主体放在新行中,缩进一级:

// ✔️ OK
let printListWithOffset a list1 =
    List.iter
        (fun elem ->
             printfn $"A very long line to format the value: %d{a + elem}")
        list1

如果 Lambda 参数是函数应用程序中的最后一个参数,则将所有参数放在同一行,直到出现箭头为止。

// ✔️ OK
Target.create "Build" (fun ctx ->
    // code
    // here
    ())

// ✔️ OK
let printListWithOffsetPiped a list1 =
    list1
    |> List.map (fun x -> x + 1)
    |> List.iter (fun elem ->
        printfn $"A very long line to format the value: %d{a + elem}")

以类似的方式处理 match Lambda。

// ✔️ OK
functionName arg1 arg2 arg3 (function
    | Choice1of2 x -> 1
    | Choice2of2 y -> 2)

如果 Lambda 之前有许多前导或多行参数,所有参数都缩进一级。

// ✔️ OK
functionName
    arg1
    arg2
    arg3
    (fun arg4 ->
        bodyExpr)

// ✔️ OK
functionName
    arg1
    arg2
    arg3
    (function
     | Choice1of2 x -> 1
     | Choice2of2 y -> 2)

如果 Lambda 表达式的主体长度为多行,则应考虑将其重构为局部范围的函数。

当管道包含 Lambda 表达式时,每个 Lambda 表达式通常都是管道的每个阶段的最后一个参数:

// ✔️ OK, with 4 spaces indentation
let printListWithOffsetPiped list1 =
    list1
    |> List.map (fun elem -> elem + 1)
    |> List.iter (fun elem ->
        // one indent starting from the pipe
        printfn $"A very long line to format the value: %d{elem}")

// ✔️ OK, with 2 spaces indentation
let printListWithOffsetPiped list1 =
  list1
  |> List.map (fun elem -> elem + 1)
  |> List.iter (fun elem ->
    // one indent starting from the pipe
    printfn $"A very long line to format the value: %d{elem}")

如果 lambda 的参数不放在单行中,或者本身是多行,则将其放在下一行,并缩进一级。

// ✔️ OK
fun
    (aVeryLongParameterName: AnEquallyLongTypeName)
    (anotherVeryLongParameterName: AnotherLongTypeName)
    (yetAnotherLongParameterName: LongTypeNameAsWell)
    (youGetTheIdeaByNow: WithLongTypeNameIncluded) ->
    // code starts here
    ()

// ❌ Not OK, code formatters will reformat to the above to respect the maximum line length.
fun (aVeryLongParameterName: AnEquallyLongTypeName) (anotherVeryLongParameterName: AnotherLongTypeName) (yetAnotherLongParameterName: LongTypeNameAsWell) (youGetTheIdeaByNow: WithLongTypeNameIncluded) ->
    ()

// ✔️ OK
let useAddEntry () =
    fun
        (input:
            {| name: string
               amount: Amount
               isIncome: bool
               created: string |}) ->
         // foo
         bar ()

// ❌ Not OK, code formatters will reformat to the above to avoid reliance on whitespace alignment that is contingent to length of an identifier.
let useAddEntry () =
    fun (input: {| name: string
                   amount: Amount
                   isIncome: bool
                   created: string |}) ->
        // foo
        bar ()

设置算术和二进制表达式的格式

在二进制算术表达式周围始终使用空格:

// ✔️ OK
let subtractThenAdd x = x - 1 + 3

与某些格式设置选项结合使用时,如果未能将二元 - 运算符括起来,则可能会导致将其解释为一元 -。 一元 - 运算符后应始终紧跟它们要取反的值:

// ✔️ OK
let negate x = -x

// ❌ Not OK
let negateBad x = - x

- 运算符之后添加空格字符可能会导致其他人混淆。

用空格分隔二元运算符。 中缀表达式可以排列在同一列:

// ✔️ OK
let function1 () =
    acc +
    (someFunction
         x.IngredientName x.Quantity)

// ✔️ OK
let function1 arg1 arg2 arg3 arg4 =
    arg1 + arg2 +
    arg3 + arg4

此规则也适用于类型和常量批注中的度量单位:

// ✔️ OK
type Test =
    { WorkHoursPerWeek: uint<hr / (staff weeks)> }
    static member create = { WorkHoursPerWeek = 40u<hr / (staff weeks)> }

// ❌ Not OK
type Test =
    { WorkHoursPerWeek: uint<hr/(staff weeks)> }
    static member create = { WorkHoursPerWeek = 40u<hr/(staff weeks)> }

应使用以下在 F# 标准库中定义的运算符,而不是定义等效项。 建议使用这些运算符,因为它们会使代码更具可读性和惯用性。 以下列表总结了推荐的 F# 运算符。

// ✔️ OK
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

设置范围运算符表达式的格式

仅当所有表达式均为非原子时,才在 .. 周围添加空格。 整数和单字标识符被认为是原子的。

// ✔️ OK
let a = [ 2..7 ] // integers
let b = [ one..two ] // identifiers
let c = [ ..9 ] // also when there is only one expression
let d = [ 0.7 .. 9.2 ] // doubles
let e = [ 2L .. number / 2L ] // complex expression
let f = [| A.B .. C.D |] // identifiers with dots
let g = [ .. (39 - 3) ] // complex expression
let h = [| 1 .. MyModule.SomeConst |] // not all expressions are atomic

for x in 1..2 do
    printfn " x = %d" x

let s = seq { 0..10..100 }

// ❌ Not OK
let a = [ 2 .. 7 ]
let b = [ one .. two ]

这些规则也适用于切片:

// ✔️ OK
arr[0..10]
list[..^1]

设置 if 表达式格式

条件句的缩进取决于构成它们的表达式的大小和复杂性。 在以下情况下,将它们写在一行:

  • conde1e2 很短。
  • e1e2 本身不是 if/then/else 表达式。
// ✔️ OK
if cond then e1 else e2

如果不存在 else 表达式,建议不要在一行中编写整个表达式。 这是为了区分命令性代码与功能性代码。

// ✔️ OK
if a then
    ()

// ❌ Not OK, code formatters will reformat to the above by default
if a then ()

如果任何一个表达式都是多行的,则每个条件分支都应该是多行的。

// ✔️ OK
if cond then
    let e1 = something()
    e1
else
    e2
    
// ❌ Not OK
if cond then
    let e1 = something()
    e1
else e2

带有 elifelse 的多个条件句在遵循一行 if/then/else 表达式的规则时,其缩进范围与 if 相同。

// ✔️ OK
if cond1 then e1
elif cond2 then e2
elif cond3 then e3
else e4

如果任何条件或表达式是多行的,则整个 if/then/else 表达式是多行的:

// ✔️ OK
if cond1 then
    let e1 = something()
    e1
elif cond2 then
    e2
elif cond3 then
    e3
else
    e4

// ❌ Not OK
if cond1 then
    let e1 = something()
    e1
elif cond2 then e2
elif cond3 then e3
else e4

如果条件跨多行或超过一行的默认容许长度,则条件表达式应使用一个字符的缩进和一个新行。 封装较长的条件表达式时,ifthen 关键字应该相符。

// ✔️ OK, but better to refactor, see below
if
    complexExpression a b && env.IsDevelopment()
    || someFunctionToCall
        aVeryLongParameterNameOne
        aVeryLongParameterNameTwo
        aVeryLongParameterNameThree 
then
        e1
    else
        e2

// ✔️The same applies to nested `elif` or `else if` expressions
if a then
    b
elif
    someLongFunctionCall
        argumentOne
        argumentTwo
        argumentThree
        argumentFour
then
    c
else if
    someOtherLongFunctionCall
        argumentOne
        argumentTwo
        argumentThree
        argumentFour
then
    d

但最好将长条件重构为 let 绑定或单独的函数:

// ✔️ OK
let performAction =
    complexExpression a b && env.IsDevelopment()
    || someFunctionToCall
        aVeryLongParameterNameOne
        aVeryLongParameterNameTwo
        aVeryLongParameterNameThree

if performAction then
    e1
else
    e2

设置联合用例表达式的格式

应用可区分的联合用例遵循与函数和方法应用程序相同的规则。 也就是说,因为名称是大写的,代码格式化程序将删除元组前的空格:

// ✔️ OK
let opt = Some("A", 1)

// OK, but code formatters will remove the space
let opt = Some ("A", 1)

像函数应用程序一样,拆分为多行的结构应使用缩进:

// ✔️ OK
let tree1 =
    BinaryNode(
        BinaryNode (BinaryValue 1, BinaryValue 2),
        BinaryNode (BinaryValue 3, BinaryValue 4)
    )

设置列表和数组表达式的格式

撰写 x :: l,在 :: 运算符周围添加空格(:: 是中缀运算符,因此会被空格包围)。

在单行中声明的列表和数组应该在左方括号之后和右方括号之前添加空格:

// ✔️ OK
let xs = [ 1; 2; 3 ]

// ✔️ OK
let ys = [| 1; 2; 3; |]

始终在两个不同的类似大括号的运算符之间使用至少一个空格。 例如,在 [{ 之间留一个空格。

// ✔️ OK
[ { Ingredient = "Green beans"; Quantity = 250 }
  { Ingredient = "Pine nuts"; Quantity = 250 }
  { Ingredient = "Feta cheese"; Quantity = 250 }
  { Ingredient = "Olive oil"; Quantity = 10 }
  { Ingredient = "Lemon"; Quantity = 1 } ]

// ❌ Not OK
[{ Ingredient = "Green beans"; Quantity = 250 }
 { Ingredient = "Pine nuts"; Quantity = 250 }
 { Ingredient = "Feta cheese"; Quantity = 250 }
 { Ingredient = "Olive oil"; Quantity = 10 }
 { Ingredient = "Lemon"; Quantity = 1 }]

相同的指南适用于元组的列表或数组。

拆分为多行的列表和数组遵循与记录类似的规则:

// ✔️ OK
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 |] |]

与记录一样,在左方括号和右方括号各自所在的行上声明它们将使移动代码和通过管道移入函数中更为容易:

// ✔️ OK
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 |] 
    |]

如果列表或数组表达式位于绑定的右侧,你可能更喜欢使用 Stroustrup 样式:

// ✔️ OK
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 |] 
|]

但是,如果列表或数组表达式不在绑定的右侧,例如当它位于另一个列表或数组的内部时,如果该内部表达式需要跨越多行,则括号应位于其所在的行中:

// ✔️ OK - The outer list follows `Stroustrup` style, while the inner lists place their brackets on separate lines
let fn a b = [ 
    [
        someReallyLongValueThatWouldForceThisListToSpanMultipleLines
        a
    ]
    [ 
        b
        someReallyLongValueThatWouldForceThisListToSpanMultipleLines 
    ]
]

// ❌ Not okay
let fn a b = [ [
    someReallyLongValueThatWouldForceThisListToSpanMultipleLines
    a
]; [
    b
    someReallyLongValueThatWouldForceThisListToSpanMultipleLines
] ]

同样的规则也适用于数组/列表中的记录类型:

// ✔️ OK - The outer list follows `Stroustrup` style, while the inner lists place their brackets on separate lines
let fn a b = [ 
    {
        Foo = someReallyLongValueThatWouldForceThisListToSpanMultipleLines
        Bar = a
    }
    { 
        Foo = b
        Bar = someReallyLongValueThatWouldForceThisListToSpanMultipleLines 
    }
]

// ❌ Not okay
let fn a b = [ {
    Foo = someReallyLongValueThatWouldForceThisListToSpanMultipleLines
    Bar = a
}; {
    Foo = b
    Bar = someReallyLongValueThatWouldForceThisListToSpanMultipleLines
} ]

当以编程方式生成数组和列表时,如果总是生成一个值,则最好使用 -> 而不是 do ... yield

// ✔️ OK
let squares = [ for x in 1..10 -> x * x ]

// ❌ Not preferred, use "->" when a value is always generated
let squares' = [ for x in 1..10 do yield x * x ]

在可能有条件地生成数据或可能有连续表达式要计算的情况下,旧版 F# 需要指定 yield。 除非必须使用较旧的 F# 语言版本进行编译,否则最好省略这些 yield 关键字:

// ✔️ OK
let daysOfWeek includeWeekend =
    [
        "Monday"
        "Tuesday"
        "Wednesday"
        "Thursday"
        "Friday"
        if includeWeekend then
            "Saturday"
            "Sunday"
    ]

// ❌ Not preferred - omit yield instead
let daysOfWeek' includeWeekend =
    [
        yield "Monday"
        yield "Tuesday"
        yield "Wednesday"
        yield "Thursday"
        yield "Friday"
        if includeWeekend then
            yield "Saturday"
            yield "Sunday"
    ]

在某些情况下,do...yield 可能有助于提高可读性。 这些情况虽然是主观的,但应予以考虑。

设置记录表达式格式

短记录可写在一行中:

// ✔️ OK
let point = { X = 1.0; Y = 0.0 }

较长的记录应为标签使用新行:

// ✔️ OK
let rainbow =
    { Boss = "Jeffrey"
      Lackeys = ["Zippy"; "George"; "Bungle"] }

多行括号格式设置样式

对于跨越多行的记录,有三种常用的格式设置样式:CrampedAlignedStroustrupCramped 样式一直是 F# 代码的默认样式,因为它倾向于使用允许编译器轻松分析代码的样式。 AlignedStroustrup 样式都允许更轻松地重新排序成员,从而使代码更容易重构,缺点是某些情况可能需要稍微冗长的代码。

  • Cramped:历史标准,默认的 F# 记录格式。 左括号与第一个成员在同一行,右括号与最后一个成员在同一行。

    let rainbow = 
        { Boss1 = "Jeffrey"
          Boss2 = "Jeffrey"
          Boss3 = "Jeffrey"
          Lackeys = [ "Zippy"; "George"; "Bungle" ] }
    
  • Aligned:每个括号都有自己的行,在同一列上对齐。

    let rainbow =
        {
            Boss1 = "Jeffrey"
            Boss2 = "Jeffrey"
            Boss3 = "Jeffrey"
            Lackeys = ["Zippy"; "George"; "Bungle"]
        }
    
  • Stroustrup:左括号与绑定在同一行,右括号位于自己所在的行。

    let rainbow = {
        Boss1 = "Jeffrey"
        Boss2 = "Jeffrey"
        Boss3 = "Jeffrey"
        Lackeys = [ "Zippy"; "George"; "Bungle" ]
    }
    

相同的格式设置样式规则也适用于列表和数组元素。

设置复制和更新记录表达式的格式

复制和更新记录表达式仍然是记录,因此适用类似的指南。

短表达式可放在一行中:

// ✔️ OK
let point2 = { point with X = 1; Y = 2 }

较长的表达式应使用新的行,并根据上述约定之一设置格式:

// ✔️ OK - Cramped
let newState =
    { state with
        Foo =
            Some
                { F1 = 0
                  F2 = "" } }
        
// ✔️ OK - Aligned
let newState = 
    {
        state with
            Foo =
                Some
                    {
                        F1 = 0
                        F2 = ""
                    }
    }

// ✔️ OK - Stroustrup
let newState = { 
    state with
        Foo =
            Some { 
                F1 = 0
                F2 = ""
            }
}

注意:如果对复制和更新表达式使用 Stroustrup 样式,则必须进一步缩进成员而不是复制的记录名称:

// ✔️ OK
let bilbo = {
    hobbit with 
        Name = "Bilbo"
        Age = 111
        Region = "The Shire" 
}

// ❌ Not OK - Results in compiler error: "Possible incorrect indentation: this token is offside of context started at position"
let bilbo = {
    hobbit with 
    Name = "Bilbo"
    Age = 111
    Region = "The Shire" 
}

设置模式匹配格式

对于没有缩进的 match 的每个子句,使用 |。 如果表达式很短,并且每个子表达式也很简单,则可以考虑使用单行。

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

// ❌ Not OK, code formatters will reformat to the above by default
match l with
    | { him = x; her = "Posh" } :: tail -> x
    | _ :: tail -> findDavid tail
    | [] -> failwith "Couldn't find David"

如果模式匹配箭头右侧的表达式太大,则将其移至下一行,并从 match/| 缩进一级。

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

类似于较大的 if 条件,如果 match 表达式跨多行或超过一行的默认容许长度,则 match 表达式应使用一个字符的缩进和一个新行。 封装较长的 match 表达式时,matchwith 关键字应该相符。

// ✔️ OK, but better to refactor, see below
match
    complexExpression a b && env.IsDevelopment()
    || someFunctionToCall
        aVeryLongParameterNameOne
        aVeryLongParameterNameTwo
        aVeryLongParameterNameThree 
with
| X y -> y
| _ -> 0

但最好将长 match 表达式重构为 let 绑定或单独的函数:

// ✔️ OK
let performAction =
    complexExpression a b && env.IsDevelopment()
    || someFunctionToCall
        aVeryLongParameterNameOne
        aVeryLongParameterNameTwo
        aVeryLongParameterNameThree

match performAction with
| X y -> y
| _ -> 0

应避免对齐模式匹配的箭头。

// ✔️ OK
match lam with
| Var v -> v.Length
| Abstraction _ -> 2

// ❌ Not OK, code formatters will reformat to the above by default
match lam with
| Var v         -> v.Length
| Abstraction _ -> 2

使用关键字 function 引入的模式匹配应该从上一行的开头缩进一级:

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

通常应避免在 letlet rec 定义的函数中使用 function,而应使用 match。 如果使用,则模式规则应与关键字 function 对齐:

// ✔️ OK
let rec sizeLambda acc =
    function
    | Abs(x, body) -> sizeLambda (succ acc) body
    | App(lam1, lam2) -> sizeLambda (sizeLambda acc lam1) lam2
    | Var v -> succ acc

设置 try/with 表达式格式

异常类型上的模式匹配应该缩进到与 with 相同的级别。

// ✔️ 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"

为每个子句添加 |,除非只有一个子句:

// ✔️ OK
try
    persistState currentState
with ex ->
    printfn "Something went wrong: %A" ex

// ✔️ OK
try
    persistState currentState
with :? System.ApplicationException as ex ->
    printfn "Something went wrong: %A" ex

// ❌ Not OK, see above for preferred formatting
try
    persistState currentState
with
| ex ->
    printfn "Something went wrong: %A" ex

// ❌ Not OK, see above for preferred formatting
try
    persistState currentState
with
| :? System.ApplicationException as ex ->
    printfn "Something went wrong: %A" ex

设置命名参数格式

命名参数应该在 = 周围添加空格:

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

// ❌ Not OK, spaces are necessary around '=' for named arguments
let makeStreamReader x = new System.IO.StreamReader(path=x)

例如,当使用可区分联合进行模式匹配时,命名模式的格式类似。

type Data =
    | TwoParts of part1: string * part2: string
    | OnePart of part1: string

// ✔️ OK
let examineData x =
    match data with
    | OnePartData(part1 = p1) -> p1
    | TwoPartData(part1 = p1; part2 = p2) -> p1 + p2

// ❌ Not OK, spaces are necessary around '=' for named pattern access
let examineData x =
    match data with
    | OnePartData(part1=p1) -> p1
    | TwoPartData(part1=p1; part2=p2) -> p1 + p2

设置变化表达式格式

变化表达式 location <- expr 通常采用一行的格式。 如果需要多行格式,请将右侧表达式放在新行中。

// ✔️ OK
ctx.Response.Headers[HeaderNames.ContentType] <-
    Constants.jsonApiMediaType |> StringValues

ctx.Response.Headers[HeaderNames.ContentLength] <-
    bytes.Length |> string |> StringValues

// ❌ Not OK, code formatters will reformat to the above by default
ctx.Response.Headers[HeaderNames.ContentType] <- Constants.jsonApiMediaType
                                                 |> StringValues
ctx.Response.Headers[HeaderNames.ContentLength] <- bytes.Length
                                                   |> string
                                                   |> StringValues

设置对象表达式格式

对象表达式成员应与 member 对齐,并缩进一级。

// ✔️ OK
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) }

你可能还希望使用 Stroustrup 样式:

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

可以在一行上设置空类型定义的格式:

type AnEmptyType = class end

无论选择的页面宽度如何,= class end 应始终在同一行上。

设置索引/切片表达式的格式

索引表达式不应在左方括号和右方括号周围包含任何空格。

// ✔️ OK
let v = expr[idx]
let y = myList[0..1]

// ❌ Not OK
let v = expr[ idx ]
let y = myList[ 0 .. 1 ]

这也适用于较早的 expr.[idx] 语法。

// ✔️ OK
let v = expr.[idx]
let y = myList.[0..1]

// ❌ Not OK
let v = expr.[ idx ]
let y = myList.[ 0 .. 1 ]

设置带引号的表达式的格式

如果带引号的表达式是多行表达式,则分隔符符号(<@@><@@@@>)应放在单独的行上。

// ✔️ OK
<@
    let f x = x + 10
    f 20
@>

// ❌ Not OK
<@ let f x = x + 10
   f 20
@>

在单行表达式中,应将分隔符放在表达式本身所在的行上。

// ✔️ OK
<@ 1 + 1 @>

// ❌ Not OK
<@
    1 + 1
@>

设置链式表达式的格式

如果链式表达式(与 . 交织在一起的函数应用程序)很长,则将每个应用程序调用放在下一行。 在前导链接之后将链中的后续链接缩进一级。

// ✔️ OK
Host
    .CreateDefaultBuilder(args)
    .ConfigureWebHostDefaults(fun webBuilder -> webBuilder.UseStartup<Startup>())

// ✔️ OK
Cli
    .Wrap("git")
    .WithArguments(arguments)
    .WithWorkingDirectory(__SOURCE_DIRECTORY__)
    .ExecuteBufferedAsync()
    .Task

如果前导链接是简单的标识符,则可以由多个链接组成。 例如,添加完全限定的命名空间。

// ✔️ OK
Microsoft.Extensions.Hosting.Host
    .CreateDefaultBuilder(args)
    .ConfigureWebHostDefaults(fun webBuilder -> webBuilder.UseStartup<Startup>())

后续链接还应包含简单的标识符。

// ✔️ OK
configuration.MinimumLevel
    .Debug()
    // Notice how `.WriteTo` does not need its own line.
    .WriteTo.Logger(fun loggerConfiguration ->
        loggerConfiguration.Enrich
            .WithProperty("host", Environment.MachineName)
            .Enrich.WithProperty("user", Environment.UserName)
            .Enrich.WithProperty("application", context.HostingEnvironment.ApplicationName))

如果函数应用程序中的参数不适合该行的其余部分,请将每个参数放在下一行。

// ✔️ OK
WebHostBuilder()
    .UseKestrel()
    .UseUrls("http://*:5000/")
    .UseCustomCode(
        longArgumentOne,
        longArgumentTwo,
        longArgumentThree,
        longArgumentFour
    )
    .UseContentRoot(Directory.GetCurrentDirectory())
    .UseStartup<Startup>()
    .Build()

// ✔️ OK
Cache.providedTypes
    .GetOrAdd(cacheKey, addCache)
    .Value

// ❌ Not OK, formatting tools will reformat to the above
Cache
    .providedTypes
    .GetOrAdd(
        cacheKey,
        addCache
    )
    .Value

函数应用程序中的 Lambda 参数应与开头 ( 在同一行开始。

// ✔️ OK
builder
    .WithEnvironment()
    .WithLogger(fun loggerConfiguration ->
        // ...
        ())

// ❌ Not OK, formatting tools will reformat to the above
builder
    .WithEnvironment()
    .WithLogger(
        fun loggerConfiguration ->
        // ...
        ())

设置声明格式

本部分介绍如何设置不同类型的声明的格式。

在声明之间添加空行

使用一个空行分隔顶级函数和类定义。 例如:

// ✔️ OK
let thing1 = 1+1

let thing2 = 1+2

let thing3 = 1+3

type ThisThat = This | That

// ❌ Not OK
let thing1 = 1+1
let thing2 = 1+2
let thing3 = 1+3
type ThisThat = This | That

如果构造具有 XML 文档注释,请在注释前添加一个空行。

// ✔️ OK

/// This is a function
let thisFunction() =
    1 + 1

/// This is another function, note the blank line before this line
let thisFunction() =
    1 + 1

设置 let 和 member 声明的格式

设置 letmember 声明的格式时,通常绑定的右侧要么在一行上,要么(如果太长)使用新行,并缩进一级。

例如,以下示例符合要求:

// ✔️ OK
let a =
    """
foobar, long string
"""

// ✔️ OK
type File =
    member this.SaveAsync(path: string) : Async<unit> =
        async {
            // IO operation
            return ()
        }

// ✔️ OK
let c =
    { Name = "Bilbo"
      Age = 111
      Region = "The Shire" }

// ✔️ OK
let d =
    while f do
        printfn "%A" x

这些不符合要求:

// ❌ Not OK, code formatters will reformat to the above by default
let a = """
foobar, long string
"""

let d = while f do
    printfn "%A" x

记录类型实例化也可以将括号放在其所在的行上:

// ✔️ OK
let bilbo =
    { 
        Name = "Bilbo"
        Age = 111
        Region = "The Shire" 
    }

你可能还更偏好使用 Stroustrup 样式,开头 { 与绑定名称位于同一行:

// ✔️ OK
let bilbo = {
    Name = "Bilbo"
    Age = 111
    Region = "The Shire"
}

使用一个空行和文档分隔 member,并添加文档注释:

// ✔️ OK

/// This is a thing
type ThisThing(value: int) =

    /// Gets the value
    member _.Value = value

    /// Returns twice the value
    member _.TwiceValue() = value*2

可以(谨慎地)使用额外的空行来分隔相关功能组。 在一组相关的单行代码之间可以省略空行(例如,一组虚拟实现)。 在函数中慎用空行来指示逻辑部分。

设置 function 和 member 参数的格式

定义函数时,请在每个参数周围使用空格。

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

// ❌ Not OK, code formatters will reformat to the above by default
let myFunBad (a:decimal)(b:int)c = a + b + c

如果函数定义很长,请将参数放在新行中并缩进,以与后续参数的缩进级别保持一致。

// ✔️ OK
module M =
    let longFunctionWithLotsOfParameters
        (aVeryLongParam: AVeryLongTypeThatYouNeedToUse)
        (aSecondVeryLongParam: AVeryLongTypeThatYouNeedToUse)
        (aThirdVeryLongParam: AVeryLongTypeThatYouNeedToUse)
        =
        // ... the body of the method follows

    let longFunctionWithLotsOfParametersAndReturnType
        (aVeryLongParam: AVeryLongTypeThatYouNeedToUse)
        (aSecondVeryLongParam: AVeryLongTypeThatYouNeedToUse)
        (aThirdVeryLongParam: AVeryLongTypeThatYouNeedToUse)
        : ReturnType =
        // ... the body of the method follows

    let longFunctionWithLongTupleParameter
        (
            aVeryLongParam: AVeryLongTypeThatYouNeedToUse,
            aSecondVeryLongParam: AVeryLongTypeThatYouNeedToUse,
            aThirdVeryLongParam: AVeryLongTypeThatYouNeedToUse
        ) =
        // ... the body of the method follows

    let longFunctionWithLongTupleParameterAndReturnType
        (
            aVeryLongParam: AVeryLongTypeThatYouNeedToUse,
            aSecondVeryLongParam: AVeryLongTypeThatYouNeedToUse,
            aThirdVeryLongParam: AVeryLongTypeThatYouNeedToUse
        ) : ReturnType =
        // ... the body of the method follows

这也适用于使用元组的成员、构造函数和参数:

// ✔️ OK
type TypeWithLongMethod() =
    member _.LongMethodWithLotsOfParameters
        (
            aVeryLongParam: AVeryLongTypeThatYouNeedToUse,
            aSecondVeryLongParam: AVeryLongTypeThatYouNeedToUse,
            aThirdVeryLongParam: AVeryLongTypeThatYouNeedToUse
        ) =
        // ... the body of the method

// ✔️ OK
type TypeWithLongConstructor
    (
        aVeryLongCtorParam: AVeryLongTypeThatYouNeedToUse,
        aSecondVeryLongCtorParam: AVeryLongTypeThatYouNeedToUse,
        aThirdVeryLongCtorParam: AVeryLongTypeThatYouNeedToUse
    ) =
    // ... the body of the class follows

// ✔️ OK
type TypeWithLongSecondaryConstructor () =
    new
        (
            aVeryLongCtorParam: AVeryLongTypeThatYouNeedToUse,
            aSecondVeryLongCtorParam: AVeryLongTypeThatYouNeedToUse,
            aThirdVeryLongCtorParam: AVeryLongTypeThatYouNeedToUse
        ) =
        // ... the body of the constructor follows

如果参数经过柯里化,请将 = 字符与任何返回类型一起放在新行中:

// ✔️ OK
type TypeWithLongCurriedMethods() =
    member _.LongMethodWithLotsOfCurriedParamsAndReturnType
        (aVeryLongParam: AVeryLongTypeThatYouNeedToUse)
        (aSecondVeryLongParam: AVeryLongTypeThatYouNeedToUse)
        (aThirdVeryLongParam: AVeryLongTypeThatYouNeedToUse)
        : ReturnType =
        // ... the body of the method

    member _.LongMethodWithLotsOfCurriedParams
        (aVeryLongParam: AVeryLongTypeThatYouNeedToUse)
        (aSecondVeryLongParam: AVeryLongTypeThatYouNeedToUse)
        (aThirdVeryLongParam: AVeryLongTypeThatYouNeedToUse)
        =
        // ... the body of the method

这是一种避免行过长(如果返回类型的名称可能很长)并且在添加参数时减少行损坏的方法。

设置运算符声明格式

可以选择在运算符定义周围使用空格:

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

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

对于任何以 * 开头且具有多个字符的自定义运算符,需要在定义的开头添加一个空格以避免编译器歧义。 因此,我们建议只需在所有运算符的定义周围使用一个空格字符。

设置记录声明的格式

对于记录声明,默认情况下,应将类型定义中的 { 缩进四个空格,在同一行开始编写标签列表,并将成员(如果有)与 { 标记对齐:

// ✔️ OK
type PostalAddress =
    { Address: string
      City: string
      Zip: string }

通常更偏好将括号放在其所在的行上,标签由额外的四个空格缩进:

// ✔️ OK
type PostalAddress =
    { 
        Address: string
        City: string
        Zip: string 
    }

你还可以将 { 放在类型定义第一行的末尾(Stroustrup 样式):

// ✔️ OK
type PostalAddress = {
    Address: string
    City: string
    Zip: string
}

如果需要其他成员,请尽量不要使用 with/end

// ✔️ OK
type PostalAddress =
    { Address: string
      City: string
      Zip: string }
    member x.ZipAndCity = $"{x.Zip} {x.City}"

// ❌ Not OK, code formatters will reformat to the above by default
type PostalAddress =
    { Address: string
      City: string
      Zip: string }
  with
    member x.ZipAndCity = $"{x.Zip} {x.City}"
  end
  
// ✔️ OK
type PostalAddress =
    { 
        Address: string
        City: string
        Zip: string 
    }
    member x.ZipAndCity = $"{x.Zip} {x.City}"
    
// ❌ Not OK, code formatters will reformat to the above by default
type PostalAddress =
    { 
        Address: string
        City: string
        Zip: string 
    }
    with
        member x.ZipAndCity = $"{x.Zip} {x.City}"
    end

此样式规则的例外情况为是否根据 Stroustrup 样式设置记录格式。 在这种情况下,由于编译器规则,如果要实现接口或添加其他成员,则需要 with 关键字:

// ✔️ OK
type PostalAddress = {
    Address: string
    City: string
    Zip: string
} with
    member x.ZipAndCity = $"{x.Zip} {x.City}"
   
// ❌ Not OK, this is currently invalid F# code
type PostalAddress = {
    Address: string
    City: string
    Zip: string
} 
member x.ZipAndCity = $"{x.Zip} {x.City}"

如果为记录字段添加了 XML 文档,首选 AlignedStroustrup 样式,并且应在成员之间添加额外的空格:

// ❌ Not OK - putting { and comments on the same line should be avoided
type PostalAddress =
    { /// The address
      Address: string

      /// The city
      City: string

      /// The zip code
      Zip: string }

    /// Format the zip code and the city
    member x.ZipAndCity = $"{x.Zip} {x.City}"

// ✔️ OK
type PostalAddress =
    {
        /// The address
        Address: string

        /// The city
        City: string

        /// The zip code
        Zip: string
    }

    /// Format the zip code and the city
    member x.ZipAndCity = $"{x.Zip} {x.City}"

// ✔️ OK - Stroustrup Style
type PostalAddress = {
    /// The address
    Address: string

    /// The city
    City: string

    /// The zip code
    Zip: string
} with
    /// Format the zip code and the city
    member x.ZipAndCity = $"{x.Zip} {x.City}"

如果要在记录中声明接口实现或成员,最好将开始标记放在一个新行中,将结束标记放在一个新行中:

// ✔️ OK
// Declaring additional members on PostalAddress
type PostalAddress =
    {
        /// The address
        Address: string

        /// The city
        City: string

        /// The zip code
        Zip: string
    }

    member x.ZipAndCity = $"{x.Zip} {x.City}"

// ✔️ OK
type MyRecord =
    {
        /// The record field
        SomeField: int
    }
    interface IMyInterface

这些相同的规则也适用于匿名记录类型别名。

设置可区分联合声明的格式

对于可区分联合声明,将类型定义中的 | 缩进四个空格:

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

如果只有一个短联合,则可以省略前导 |

// ✔️ OK
type Address = Address of string

对于较长的联合或多行联合,保留 |,并将每个联合字段放在一个新行中,并在每行的末尾使用 * 分隔。

// ✔️ OK
[<NoEquality; NoComparison>]
type SynBinding =
    | SynBinding of
        accessibility: SynAccess option *
        kind: SynBindingKind *
        mustInline: bool *
        isMutable: bool *
        attributes: SynAttributes *
        xmlDoc: PreXmlDoc *
        valData: SynValData *
        headPat: SynPat *
        returnInfo: SynBindingReturnInfo option *
        expr: SynExpr *
        range: range *
        seqPoint: DebugPointAtBinding

添加文档注释时,在每个 /// 注释之前使用空行。

// ✔️ OK

/// The volume
type Volume =

    /// The volume in liters
    | Liter of float

    /// The volume in fluid ounces
    | FluidOunce of float

    /// The volume in imperial pints
    | ImperialPint of float

设置文本声明的格式

使用 Literal 属性的 F# 文本应将该属性放在其自己的行上,并使用 PascalCase 命名法:

// ✔️ OK

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

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

避免将属性与值放在同一行中。

设置模块声明的格式

本地模块中的代码必须相对于 module 缩进,但顶级模块中的代码不应缩进。 命名空间元素不需要缩进。

// ✔️ OK - A is a top-level module.
module A

let function1 a b = a - b * b
// ✔️ OK - 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

设置 do 声明的格式

在类型声明、模块声明和计算表达式中,使用 dodo! 有时需要进行副作用运算。 当这些内容跨多行时,使用缩进和新行来保持缩进与 let/let! 一致。 下面是在类中使用 do 的示例:

// ✔️ OK
type Foo() =
    let foo =
        fooBarBaz
        |> loremIpsumDolorSitAmet
        |> theQuickBrownFoxJumpedOverTheLazyDog

    do
        fooBarBaz
        |> loremIpsumDolorSitAmet
        |> theQuickBrownFoxJumpedOverTheLazyDog

// ❌ Not OK - notice the "do" expression is indented one space less than the `let` expression
type Foo() =
    let foo =
        fooBarBaz
        |> loremIpsumDolorSitAmet
        |> theQuickBrownFoxJumpedOverTheLazyDog
    do fooBarBaz
       |> loremIpsumDolorSitAmet
       |> theQuickBrownFoxJumpedOverTheLazyDog

下面是使用两个空格进行缩进的 do! 示例(因为对于 do!,使用四个空格进行缩进时,碰巧这两种方法之间没有区别):

// ✔️ OK
async {
  let! foo =
    fooBarBaz
    |> loremIpsumDolorSitAmet
    |> theQuickBrownFoxJumpedOverTheLazyDog

  do!
    fooBarBaz
    |> loremIpsumDolorSitAmet
    |> theQuickBrownFoxJumpedOverTheLazyDog
}

// ❌ Not OK - notice the "do!" expression is indented two spaces more than the `let!` expression
async {
  let! foo =
    fooBarBaz
    |> loremIpsumDolorSitAmet
    |> theQuickBrownFoxJumpedOverTheLazyDog
  do! fooBarBaz
      |> loremIpsumDolorSitAmet
      |> theQuickBrownFoxJumpedOverTheLazyDog
}

设置计算表达式操作的格式

在针对计算表达式创建自定义操作时,建议使用 camelCase 命名法:

// ✔️ OK
type MathBuilder() =
    member _.Yield _ = 0

    [<CustomOperation("addOne")>]
    member _.AddOne (state: int) =
        state + 1

    [<CustomOperation("subtractOne")>]
    member _.SubtractOne (state: int) =
        state - 1

    [<CustomOperation("divideBy")>]
    member _.DivideBy (state: int, divisor: int) =
        state / divisor

    [<CustomOperation("multiplyBy")>]
    member _.MultiplyBy (state: int, factor: int) =
        state * factor

let math = MathBuilder()

let myNumber =
    math {
        addOne
        addOne
        addOne
        subtractOne
        divideBy 2
        multiplyBy 10
    }

要建模的域最终应完成命名约定。 如果习惯使用其他约定,则应改为使用该约定。

如果表达式的返回值是计算表达式,最好将计算表达式关键字名称放在其所在的行中:

// ✔️ OK
let foo () = 
    async {
        let! value = getValue()
        do! somethingElse()
        return! anotherOperation value 
    }

你可能还更偏好将计算表达式与绑定名称放在同一行:

// ✔️ OK
let foo () = async {
    let! value = getValue()
    do! somethingElse()
    return! anotherOperation value 
}

无论你喜欢哪种方式,都应致力于在整个代码库中保持一致。 借助格式化程序,可以指定此首选项以保持一致。

设置类型和类型注释的格式

本部分介绍如何设置类型和类型注释的格式。 这包括设置具有 .fsi 扩展名的签名文件的格式。

对于类型,首选泛型 (Foo<T>) 的前缀语法,但有一些特定的例外

F# 允许编写泛型类型的后缀样式(例如,int list)和前缀样式(例如,list<int>)。 后缀样式只能与单个类型参数一起使用。 始终首选 .NET 样式,但这五个特定类型除外:

  1. 对于 F# 列表,使用后缀形式:int list,而不是 list<int>
  2. 对于 F# 选项,使用后缀形式:int option,而不是 option<int>
  3. 对于 F# 值选项,使用后缀形式:int voption,而不是 voption<int>
  4. 对于 F# 数组,使用后缀形式 int array,而不是 array<int>int[]
  5. 对于引用单元格,使用 int ref,而不是 ref<int>Ref<int>

对于所有其他类型,使用前缀形式。

设置函数类型格式

定义函数的签名时,请在 -> 符号周围使用空格:

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

// ❌ Not OK
type MyFunBad = int->int->string

设置值和参数类型注释的格式

定义包含类型注释的值或参数时,请在 : 符号之后使用空格,但不能在之前使用:

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

let simpleValue: int = 0 // Type annotation for let-bound value

type C() =
    member _.Property: int = 1

// ❌ Not OK
let complexFunctionPoorlyAnnotated (a :int) (b :int) (c:int) = a + b + c
let simpleValuePoorlyAnnotated1:int = 1
let simpleValuePoorlyAnnotated2 :int = 2

设置多行类型批注的格式

类型批注很长或是多行时,将其放在下一行,并缩进一级。

type ExprFolder<'State> =
    { exprIntercept: 
        ('State -> Expr -> 'State) -> ('State -> Expr -> 'State -> 'State -> Exp -> 'State }
        
let UpdateUI
    (model:
#if NETCOREAPP2_1
        ITreeModel
#else
        TreeModel
#endif
    )
    (info: FileInfo) =
    // code
    ()

let f
    (x:
        {|
            a: Second
            b: Metre
            c: Kilogram
            d: Ampere
            e: Kelvin
            f: Mole
            g: Candela
        |})
    =
    x.a

type Sample
    (
        input: 
            LongTupleItemTypeOneThing * 
            LongTupleItemTypeThingTwo * 
            LongTupleItemTypeThree * 
            LongThingFour * 
            LongThingFiveYow
    ) =
    class
    end

对于内联匿名记录类型,还可以使用 Stroustrup 样式:

let f
    (x: {|
        x: int
        y: AReallyLongTypeThatIsMuchLongerThan40Characters
     |})
    =
    x

设置返回类型批注的格式

在函数或成员返回类型批注中,在 : 符号前后使用空格:

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

type C() =
    member _.SomeMethod(x: int) : int = 1

// ❌ Not OK
let myFunBad (a: decimal) b c:decimal = a + b + c

let anotherFunBad (arg: int): unit = ()

type C() =
    member _.SomeMethodBad(x: int): int = 1

设置签名中类型的格式

在签名中编写完整的函数类型时,有时需要将参数拆分为多行。 返回类型始终进行缩进。

对于元组函数,参数由 * 分隔,并放置在每行的末尾。

例如,请考虑具有以下实现的函数:

let SampleTupledFunction(arg1, arg2, arg3, arg4) = ...

在对应的签名文件(具有 .fsi 扩展名)中,当需要多行格式时,可将函数设置为以下格式:

// ✔️ OK
val SampleTupledFunction:
    arg1: string *
    arg2: string *
    arg3: int *
    arg4: int ->
        int list

同样,请考虑经过柯里化的函数:

let SampleCurriedFunction arg1 arg2 arg3 arg4 = ...

在对应的签名文件中,-> 位于每行的末尾:

// ✔️ OK
val SampleCurriedFunction:
    arg1: string ->
    arg2: string ->
    arg3: int ->
    arg4: int ->
        int list

同样,请考虑接受柯里化和元组参数的组合的函数:

// Typical call syntax:
let SampleMixedFunction
        (arg1, arg2)
        (arg3, arg4, arg5)
        (arg6, arg7)
        (arg8, arg9, arg10) = ..

在相应的签名文件中,元组前面的类型会进行缩进

// ✔️ OK
val SampleMixedFunction:
    arg1: string *
    arg2: string ->
        arg3: string *
        arg4: string *
        arg5: TType ->
            arg6: TType *
            arg7: TType ->
                arg8: TType *
                arg9: TType *
                arg10: TType ->
                    TType list

相同的规则适用于类型签名中的成员:

type SampleTypeName =
    member ResolveDependencies:
        arg1: string *
        arg2: string ->
            string

设置显式泛型类型参数和约束的格式

下面的指南适用于函数定义、成员定义、类型定义和函数应用程序。

如果泛型类型参数和约束不太长,请将它们保留在一行中:

// ✔️ OK
let f<'T1, 'T2 when 'T1: equality and 'T2: comparison> param =
    // function body

如果泛型类型参数/约束和函数参数均不适用,但只有类型参数/约束适用,则将参数放在新行中:

// ✔️ OK
let f<'T1, 'T2 when 'T1: equality and 'T2: comparison>
    param
    =
    // function body

如果类型参数或约束太长,请按如下所示拆分它们并将它们对齐。 将类型参数列表与函数保留在同一行中,无论其长度如何。 对于约束,将 when 放在第一行,并将每个约束保留在单行中,无论其长度如何。 将 > 放在最后一行的末尾。 将约束缩进一级。

// ✔️ OK
let inline f< ^T1, ^T2
    when ^T1: (static member Foo1: unit -> ^T2)
    and ^T2: (member Foo2: unit -> int)
    and ^T2: (member Foo3: string -> ^T1 option)>
    arg1
    arg2
    =
    // function body

如果类型参数/约束已分解,但没有正常的函数参数,则无论如何都将 = 放在新行中:

// ✔️ OK
let inline f< ^T1, ^T2
    when ^T1: (static member Foo1: unit -> ^T2)
    and ^T2: (member Foo2: unit -> int)
    and ^T2: (member Foo3: string -> ^T1 option)>
    =
    // function body

相同的规则适用于函数应用程序:

// ✔️ OK
myObj
|> Json.serialize<
    {| child: {| displayName: string; kind: string |}
       newParent: {| id: string; displayName: string |}
       requiresApproval: bool |}>

// ✔️ OK
Json.serialize<
    {| child: {| displayName: string; kind: string |}
       newParent: {| id: string; displayName: string |}
       requiresApproval: bool |}>
    myObj

设置继承的格式

基类构造函数的参数出现在 inherit 子句的参数列表中。 将 inherit 子句放在新行上,并缩进一级。

type MyClassBase(x: int) =
   class
   end

// ✔️ OK
type MyClassDerived(y: int) =
   inherit MyClassBase(y * 2)

// ❌ Not OK
type MyClassDerived(y: int) = inherit MyClassBase(y * 2)

构造函数很长或是多行时,将其放在下一行,并缩进一级。
根据多行函数应用程序的规则,设置该多行构造函数的格式。

type MyClassBase(x: string) =
   class
   end

// ✔️ OK
type MyClassDerived(y: string) =
    inherit 
        MyClassBase(
            """
            very long
            string example
            """
        )
        
// ❌ Not OK
type MyClassDerived(y: string) =
    inherit MyClassBase(
        """
        very long
        string example
        """)

设置主构造函数的格式

在默认格式设置约定中,不会在主构造函数的类型名称和括号之间添加空格。

// ✔️ OK
type MyClass() =
    class
    end

type MyClassWithParams(x: int, y: int) =
    class
    end
        
// ❌ Not OK
type MyClass () =
    class
    end

type MyClassWithParams (x: int, y: int) =
    class
    end

多个构造函数

inherit 子句是记录的一部分时,如果它很短,则将其放在同一行。 如果它很长或是多行,则将其放在下一行,并缩进一级。

type BaseClass =
    val string1: string
    new () = { string1 = "" }
    new (str) = { string1 = str }

type DerivedClass =
    inherit BaseClass

    val string2: string
    new (str1, str2) = { inherit BaseClass(str1); string2 = str2 }
    new () = 
        { inherit 
            BaseClass(
                """
                very long
                string example
                """
            )
          string2 = str2 }

设置属性格式

属性位于构造上方:

// ✔️ OK
[<SomeAttribute>]
type MyClass() = ...

// ✔️ OK
[<RequireQualifiedAccess>]
module M =
    let f x = x

// ✔️ OK
[<Struct>]
type MyRecord =
    { Label1: int
      Label2: string }

它们应位于任何 XML 文档之后:

// ✔️ OK

/// Module with some things in it.
[<RequireQualifiedAccess>]
module M =
    let f x = x

设置参数上的属性的格式

属性也可以放在参数上。 在这种情况下,将 then 放在参数所在的行中,但在名称之前:

// ✔️ OK - defines a class that takes an optional value as input defaulting to false.
type C() =
    member _.M([<Optional; DefaultParameterValue(false)>] doSomething: bool)

设置多个属性的格式

将多个属性应用于不是参数的构造时,请将每个属性放在单独的行上:

// ✔️ OK

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

应用于参数时,请将属性放在同一行上,并使用 ; 分隔符分隔它们。

致谢

这些指南基于 Anh-Dung Phan 撰写的 F# 格式设置约定的综合性指南