F# コードのフォーマットに関するガイドライン

この記事では、F# コードで次のことを実現するようにコードをフォーマットする方法のガイドラインを示します。

  • より読みやすくする
  • Visual Studio Code やその他のエディターのフォーマット ツールによって適用される規則に準拠する
  • オンラインの他のコードと同様である

コーディング規則コンポーネント デザインのガイドラインに関する記事も参照してください。これらの記事でも名前付け規則について説明しています。

コードの自動書式設定

Fantomas コード フォーマッタは、コードの自動フォーマットのための F# コミュニティの標準ツールです。 既定の設定は、このスタイル ガイドに対応しています。

このコード フォーマッタを使用することを強くお勧めします。 F# チーム内では、コード フォーマットの仕様に同意し、チーム リポジトリにチェックインされたコード フォーマッタの同意済みの設定ファイルに関して仕様を体系化する必要があります。

フォーマットについての一般的な規則

F# では、既定で有意な空白が使用され、空白が区別されます。 次のガイドラインは、これによって課される可能性のある問題の対処方法に関するガイダンスを提供することを目的としています。

タブではなくスペースを使用する

インデントが必要な場合は、タブではなく、スペースを使用する必要があります。 F# コードではタブを使用しません。文字列リテラルまたはコメントの外部でタブ文字が検出されると、コンパイラによってエラーが生成されます。

整合性のあるインデントを使用する

インデントを指定する場合は、少なくとも 1 つのスペースが必要です。 組織で、インデントに使用するスペースの数を指定するためのコーディング標準を作成できます。インデントが発生するレベルごとに、2 つ、3 つ、または 4 つのインデント スペースが一般的です。

インデントにつき 4 つのスペースをお勧めします。

ただし、プログラムのインデントは主観的な問題です。 バリエーションは問題ありませんが、従う必要がある最初の規則は、インデントの一貫性です。 一般的に受け入れられているインデントのスタイルを選択し、コードベース全体で体系的に使用します。

名前の長さの影響を受けるフォーマットを避ける

名前付けの影響を受けるインデントとアラインメントは避けるようにしてください。

// ✔️ 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 ドキュメント コメントのフォーマットについては、後述する「宣言のフォーマット」を参照してください。

式のフォーマット

このセクションでは、さまざまな種類の式のフォーマットについて説明します。

文字列式のフォーマット

文字列リテラルと補間された文字列は、行の長さに関係なく、1 行のままにします。

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

補間式を複数行にすることは、推奨されません。 代わりに、式の結果を値にバインドし、それを補間された文字列で使用します。

タプル式のフォーマット

タプルのインスタンス化はかっこで囲み、その中の区切りコンマの後には 1 つのスペースを置く必要があります (例: (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)

読みやすさの問題や、引数リストや引数名が長すぎるという理由で、関数への引数を改行して渡す必要がある場合があります。 そのような場合は、1 レベル インデントします。

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

関数が 1 つの複数行のタプル形式の引数を受け取る場合は、各引数を新しい行に配置します。

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

引数式が短い場合は、引数をスペースで区切り、1 行に収めます。

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

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

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

引数式が長い場合は、左かっこにインデントするのではなく、改行を使用して 1 レベル インデントします。

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

複数行の引数 (複数行の文字列を含む) が 1 つしかない場合でも、同じルールが該当します。

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

ラムダ式のフォーマット

ラムダ式が複数行の式の引数として使用され、他の引数が続く場合は、ラムダ式の本体を新しい行に配置し、1 レベルでインデントします。

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

ラムダの引数が関数アプリケーションの最後の引数の場合、同じ行の矢印まですべての引数を配置します。

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

ラムダのマッチングを同様の方法で処理します。

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

ラムダの前に先行する引数や複数行の引数が多数ある場合は、1 レベルですべての引数をインデントします。

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

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

ラムダ式の本体が複数行の長さである場合は、ローカルにスコープ設定された関数にリファクタリングすることを検討してください。

パイプラインにラムダ式が含まれている場合、各ラムダ式は通常、パイプラインの各ステージの最後の引数です。

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

ラムダの引数が 1 行に収まらない場合、あるいはそれ自体が複数行の場合、次の行に配置し、1 レベルごとにインデントしてください。

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

2 項 - 演算子を囲まないと、特定の書式設定の選択肢と組み合わせられた場合に、単項 - として解釈される可能性があります。 単項 - 演算子では、負数化する値を常に直後に続ける必要があります。

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

この規則は、型および constant 注釈のメジャーの単位にも適用されます。

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

範囲演算子式のフォーマット

すべての式がアトミックでない場合にのみ、.. の前後にスペースを追加します。 整数と 1 単語の識別子はアトミックと見なされます。

// ✔️ 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 式のフォーマット

条件のインデントは、それらを構成する式のサイズと複雑さによって異なります。 次の場合は 1 行に記述します。

  • conde1e2 が短い。
  • e1e2 自体が if/then/else 式ではない。
// ✔️ OK
if cond then e1 else e2

else 式がない場合は、式全体を 1 行に記述しないことをお勧めします。 これは、命令型コードを関数型と区別するためです。

// ✔️ 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 を含む複数の条件は、1 行の 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

条件が複数行であるか、単一行の既定の許容範囲を超えている場合、条件式では 1 つのインデントと新しい行を使用する必要があります。 長い条件式をカプセル化するときは、if キーワードと then キーワードを揃える必要があります。

// ✔️ 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 を記述します (:: は挿入演算子であるため、スペースで囲みます)。

1 つの行で宣言されたリストと配列では、左角かっこの後と右角かっこの前にスペースが必要です。

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

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

2 つの異なる中かっこのような演算子の間には、常に少なくとも 1 つの空白を使用します。 たとえば、[{ との間にはスペースを置きます。

// ✔️ 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 によって読みやすくなることがあります。 このようなケースは、主観的ではありますが、考慮する必要があります。

レコード式のフォーマット

短いレコードは、1 行に記述できます。

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

より長いレコードは、ラベルに新しい行を使用する必要があります。

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

複数行かっこのフォーマット スタイル

複数の行にまたがるレコードの場合、一般的に使用されるフォーマット スタイルは、CrampedAlignedStroustrup の 3 つです。 Cramped スタイルは、コンパイラがコードを簡単に解析できるスタイルが優先される傾向があるため、F# コードの既定のスタイルです。 Aligned スタイルと Stroustrup スタイルの両方を使用すると、メンバーの並べ替えが容易になり、リファクタリングが容易になる可能性があるコードが生成され、特定の状況ではやや詳細なコードが必要になる場合があるという欠点があります。

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

リストおよび配列要素には、同じフォーマット スタイル ルールが適用されます。

コピーと更新のレコード式のフォーマット

コピーと更新のレコード式もレコードであるため、同様のガイドラインが該当します。

短い式は、1 行に収めることができます。

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

パターン マッチングのフォーマット

インデントがない一致のそれぞれの句に | を使用します。 式が短く、それぞれの部分式も単純なものである場合は、1 行を使用することを検討できます。

// ✔️ 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/| から 1 ステップ インデントされた次の行に移動してください。

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

if 条件が大きい場合と同様に、一致式が複数行であるか、単一行の既定の許容範囲を超えている場合は、一致式で 1 つのインデントと新しい行を使用する必要があります。 長い一致式をカプセル化するときは、match キーワードと with キーワードを揃える必要があります。

// ✔️ 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 を使用して実行されるパターン マッチングでは、前の行の先頭から 1 レベル インデントする必要があります。

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

通常、let または let 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"

句が 1 つだけの場合を除き、句ごとに | を追加します。

// ✔️ 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 は 1 行でフォーマットされます。 複数行のフォーマットが必要な場合は、右側の式を新しい行に配置します。

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

オブジェクト式のフォーマット

オブジェクト式のメンバーは、1 レベルで 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)
}

空の型の定義は、1 行に書式設定できます。

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

チェーン式の書式設定

チェーン式 (. と絡み合う関数アプリケーション) が長い場合は、各アプリケーション呼び出しを次の行に配置します。 チェーン内の後続のリンクを、先頭のリンクの後に 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

関数アプリケーション内のラムダ引数は、左かっこ ( と同じ行で開始する必要があります。

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

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

宣言のフォーマット

このセクションでは、さまざまな種類の宣言のフォーマットについて説明します。

宣言の間に空白行を追加する

最上位の関数とクラスの定義は、1 行の空白行で区切ります。 次に例を示します。

// ✔️ 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 宣言をフォーマットする場合、通常、バインディングの右側は、1 行に収められるか、または (長すぎる場合は) 1 レベル インデントされ、新しい行に移動されます。

たとえば、次の例は準拠しています。

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

1 つの空白行とドキュメントでメンバーを区切り、ドキュメント コメントを追加します。

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

* で始まり、複数の文字を含むカスタム演算子については、コンパイラのあいまいさを回避するために、定義の先頭に空白を追加する必要があります。 このため、すべての演算子の定義を 1 つの空白文字で囲むことをお勧めします。

レコード宣言のフォーマット

レコード宣言の場合、既定では、型定義の { を 4 つのスペースでインデントし、同じ行でラベル リストを開始し、メンバーがある場合は、{ トークンを使用して配置する必要があります。

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

また、ラベルをさらに 4 つのスペースでインデントして、独自の行に角かっこを記述するのも一般的です。

// ✔️ 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 ドキュメントを追加する場合は、Aligned または Stroustrup スタイルが優先され、メンバー間に空白文字を追加する必要があります。

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

匿名レコード型のエイリアスには、これらの同じ規則が適用されます。

判別共用体宣言のフォーマット

判別共用体宣言の場合は、型定義の | を 4 つのスペースでインデントします。

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

短い共用体が 1 つの場合は、先頭の | を省略できます。

// ✔️ 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# リテラルでは、属性を独自の行に配置し、パスカル ケースの名前付けを使用する必要があります。

// ✔️ 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 宣言のフォーマット

型宣言、モジュール宣言、およびコンピュテーション式では、副作用ありの操作に対して do または do! を使用しなければならない場合があります。 これらが複数の行にまたがる場合は、インデントと新しい行を使用して、インデントと 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! をスペース 2 つでインデントした例です (do! では偶然にも、スペース 4 つでインデントしたやり方とでは違いがないため)。

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

コンピュテーション式の演算のフォーマット

コンピュテーション式のカスタム操作を作成する場合は、キャメルケースの名前付けを使用することをお勧めします。

// ✔️ 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> など) の両方を許可しています。 後置のスタイルは、1 つの型引数と共にのみ使用できます。 次の 5 つの特定の型を除き、.NET スタイルを常に優先します。

  1. F# リストの場合は、list<int> ではなく後置形式 int list を使用します。
  2. F# オプションの場合は、option<int> ではなく後置形式 int option を使用します。
  3. F# の値のオプションの場合は、voption<int> ではなく後置形式 int voption を使用します。
  4. F# 配列の場合は、array<int>int[] ではなく後置形式 int array を使用します。
  5. 参照セルの場合は、ref<int>Ref<int> ではなく int ref を使用します。

それ以外のすべての型には、前置形式を使用します。

関数型のフォーマット

関数のシグネチャを定義するときは、-> シンボルの前後に空白を使用します。

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

複数行の型の注釈の書式設定

型の注釈が長い、または複数行のとき、次の行に配置し、1 レベルごとにインデントしてください。

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

明示的なジェネリック型引数と制約のフォーマット

以下のガイドラインは、関数定義、メンバー定義、および型定義、および関数適用に適用されます。

長すぎない場合は、ジェネリック型引数と制約は 1 つの行に収めます。

// ✔️ 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 を最初の行に配置し、その長さに関わらず各制約を 1 つの行に保持します。 最後の行の末尾には > を配置します。 制約を 1 レベル インデントします。

// ✔️ 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 句を配置し、1 レベルごとにインデントします。

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)

コンストラクターが長い、または複数行の場合は、次の行に配置し、1 レベルごとにインデントします。
複数行関数アプリケーションの規則に従い、この複数行コンストラクターを書式設定します。

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 句がレコードの一部のとき、短ければ同じ行に配置します。 長ければ、あるいは複数行なら、次の行に配置し、1 レベルごとにインデントします。

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 Phan による「A comprehensive guide to F# Formatting Conventions\(F# の書式規則に関する包括的なガイド)\」に基づいています。