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 程式設計搭配使用:

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

以類似方式處理相符的 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 運算式的規則時縮排在與 if/then/else 相同的範圍。

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

格式化模式比對

針對不具縮排之相符項目的每個子句使用 |。 如果運算式是簡短的,您可以考慮在每個子運算式也很簡單時使用單行。

// ✔️ 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 條件,如果比對運算式是多行或超過單行的預設容錯,則比對運算式應該使用一個縮排和一個新行。 封裝長比對運算式時,matchwith 關鍵字應該對齊。

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

不過,更好的樣式是將長比對運算式重構為 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 和成員宣告

當格式化 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"
}

以單一空白行和文件分隔成員,並新增文件註解:

// ✔️ OK

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

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

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

可以 (謹慎地) 使用額外的空白行來分隔相關函式群組。 一群相關單行之間可能會省略空白行 (例如,一組虛擬實作)。 請謹慎地在函式中使用空白行來表示邏輯區段。

格式化函式和成員引數

定義函式時,請在每個引數周圍使用空白字元。

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

請避免將屬性放在與值相同的行上。

格式化模組宣告

本機模組中的程式碼必須相對於模組縮排,但最上層模組中的程式碼不應該縮排。 命名空間元素不需要縮排。

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

格式化參數的屬性

屬性也可以放在參數上。 在此情況下,請將其放在與參數相同的行上,並且在名稱前面:

// ✔️ 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 PhanF# 格式化慣例完整指南為基礎。