Wskazówki dotyczące formatowania kodu F#

Ten artykuł zawiera wskazówki dotyczące formatowania kodu w taki sposób, aby kod F# był następujący:

  • Bardziej czytelne
  • Zgodnie z konwencjami stosowanymi przez narzędzia formatowania w programie Visual Studio Code i innych edytorach
  • Podobnie jak w przypadku innego kodu w trybie online

Zobacz również Konwencje kodowania i Wytyczne dotyczące projektowania składników, które obejmują również konwencje nazewnictwa.

Automatyczne formatowanie kodu

Formatator kodu Fantomas to standardowe narzędzie społeczności języka F# do automatycznego formatowania kodu. Ustawienia domyślne odpowiadają temu przewodnikowi stylu.

Zdecydowanie zalecamy użycie tego formatera kodu. W zespołach F# specyfikacje formatowania kodu powinny zostać uzgodnione i skodyfikowane w zakresie uzgodnionego pliku ustawień dla formatera kodu zaewidencjonowane w repozytorium zespołu.

Ogólne reguły formatowania

Język F# domyślnie używa znaczącego odstępu i jest rozróżniany biały znak. Poniższe wytyczne mają na celu przedstawienie wskazówek dotyczących sposobu żonglowania pewnymi wyzwaniami, jakie może to narzucić.

Używanie spacji, a nie kart

Jeśli wcięcie jest wymagane, należy użyć spacji, a nie kart. Kod języka F# nie używa kart, a kompilator zwróci błąd, jeśli znak tabulacji zostanie napotkany poza literałem ciągu lub komentarzem.

Używanie spójnych wcięcia

Podczas wcięcia wymagane jest co najmniej jedno miejsce. Twoja organizacja może utworzyć standardy kodowania, aby określić liczbę spacji do użycia do wcięcia; dwa, trzy lub cztery spacje wcięcia na każdym poziomie, na którym występuje wcięcie, jest typowe.

Zalecamy stosowanie czterech spacji na wcięcie.

To powiedzenie, wcięcie programów jest subiektywną sprawą. Odmiany są ok, ale pierwszą regułą, którą należy przestrzegać, jest spójność wcięcia. Wybierz ogólnie akceptowany styl wcięcia i systematycznie używaj go w całej bazie kodu.

Unikaj formatowania wrażliwego na długość nazwy

Staraj się unikać wcięcia i wyrównania, które jest wrażliwe na nazewnictwo:

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

Główne przyczyny uniknięcia tego problemu są następujące:

  • Ważny kod jest przenoszony daleko po prawej stronie
  • Dla rzeczywistego kodu pozostało mniej szerokości
  • Zmiana nazwy może przerwać wyrównanie

Unikaj nadmiarowego odstępu

Unikaj nadmiarowego odstępu w kodzie języka F#, z wyjątkiem przypadków opisanych w tym przewodniku po stylu.

// ✔️ OK
spam (ham 1)

// ❌ Not OK
spam ( ham 1 )

Formatowanie komentarzy

Preferuj wiele komentarzy z podwójnym ukośnikiem w komentarzach blokowych.

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

Komentarze powinny wielką literę i być dobrze sformułowane frazy lub zdania.

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

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

Aby sformatować komentarze do dokumentu XML, zobacz "Deklaracje formatowania" poniżej.

Wyrażenia formatowania

W tej sekcji omówiono formatowanie wyrażeń różnych rodzajów.

Formatowanie wyrażeń ciągu

Literały ciągów i ciągi interpolowane mogą być po prostu pozostawione w jednym wierszu, niezależnie od długości wiersza.

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

Nie można zniechęcić do wyrażeń interpolowanych wielowierszowych. Zamiast tego powiąż wynik wyrażenia z wartością i użyj jej w ciągu interpolowanym.

Formatowanie wyrażeń krotki

Tworzenie wystąpienia krotki powinno być nawiasem, a rozdzielanie przecinków w nim powinno nastąpić po jednej spacji, na przykład: (1, 2), (x, y, z).

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

Często przyjmuje się, że pomija nawiasy w wzorcu dopasowywania krotki:

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

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

Często przyjmuje się również pomijanie nawiasów, jeśli krotka jest zwracaną wartością funkcji:

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

Podsumowując, preferuj tworzenie wystąpień krotki nawiasów, ale w przypadku używania krotki do dopasowywania wzorca lub wartości zwracanej jest uważana za drobną, aby uniknąć nawiasów.

Formatowanie wyrażeń aplikacji

Podczas formatowania aplikacji funkcji lub metody argumenty są podawane w tym samym wierszu, gdy szerokość wiersza zezwala na:

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

Pomiń nawiasy, chyba że argumenty wymagają ich:

// ✔️ OK
someFunction1 x.IngredientName

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

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

Nie pomijaj spacji podczas wywoływania z wieloma argumentami curried:

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

W domyślnych konwencjach formatowania miejsce jest dodawane podczas stosowania funkcji małych liter do argumentów tupled lub nawiasów (nawet w przypadku użycia pojedynczego argumentu):

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

W domyślnych konwencjach formatowania podczas stosowania metod wielkich liter do argumentów tupled nie jest dodawana żadna spacja. Jest to spowodowane tym, że są one często używane z płynnym programowaniem:

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

Może być konieczne przekazanie argumentów do funkcji w nowym wierszu jako kwestia czytelności lub dlatego, że lista argumentów lub nazwy argumentów są zbyt długie. W takim przypadku wcięcie jednego poziomu:

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

Gdy funkcja przyjmuje jeden wielowierszowy argument, umieść każdy argument w nowym wierszu:

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

Jeśli wyrażenia argumentów są krótkie, należy oddzielić argumenty spacjami i zachować je w jednym wierszu.

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

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

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

Jeśli wyrażenia argumentów są długie, użyj nowych linii i wcięcia jednego poziomu, a nie wcięcia do lewego nawiasu.

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

Te same reguły mają zastosowanie nawet wtedy, gdy istnieje tylko jeden argument wielowierszowy, w tym ciągi wielowierszowe:

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

Formatowanie wyrażeń potoku

W przypadku korzystania z wielu wierszy operatory potoków |> powinny znajdować się pod wyrażeniami, na których działają.

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

Formatowanie wyrażeń lambda

Gdy wyrażenie lambda jest używane jako argument w wyrażeniu wielowierszowym i następuje po nim inne argumenty, umieść treść wyrażenia lambda w nowym wierszu, wciętą o jeden poziom:

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

Jeśli argument lambda jest ostatnim argumentem w aplikacji funkcji, umieść wszystkie argumenty do strzałki w tym samym wierszu.

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

Traktuj match lambda's w podobny sposób.

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

Jeśli istnieje wiele argumentów wiodących lub wielowierszowych przed wcięciem lambda, wszystkie argumenty z jednym poziomem.

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

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

Jeśli treść wyrażenia lambda jest długa wieloma wierszami, należy rozważyć refaktoryzację w funkcji o zakresie lokalnym.

Gdy potoki zawierają wyrażenia lambda, każde wyrażenie lambda jest zazwyczaj ostatnim argumentem na każdym etapie potoku:

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

Jeśli argumenty lambda nie mieszczą się w jednym wierszu lub są same w sobie wielowierszowe, umieść je w następnym wierszu, wcięcie o jeden poziom.

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

Formatowanie wyrażeń arytmetycznych i binarnych

Zawsze używaj białych znaków wokół wyrażeń arytmetycznych binarnych:

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

Nie można otaczać operatora binarnego - , w połączeniu z pewnymi opcjami formatowania, może prowadzić do interpretacji go jako jednoargumentowego -. Operatory jednoargumentowe - powinny zawsze być natychmiast zgodne z wartością, którą negują:

// ✔️ OK
let negate x = -x

// ❌ Not OK
let negateBad x = - x

Dodanie znaku odstępu - po operatorze może prowadzić do nieporozumień dla innych.

Rozdziel operatory binarne spacjami. Wyrażenia infiksu są ok, aby w kolejce w tej samej kolumnie:

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

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

Ta reguła ma również zastosowanie do jednostek miar w typach i adnotacjach stałych:

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

Następujące operatory są zdefiniowane w standardowej bibliotece języka F# i powinny być używane zamiast definiowania odpowiedników. Używanie tych operatorów jest zalecane, ponieważ ma tendencję do zwiększenia czytelnego i idiotycznego kodu. Poniższa lista zawiera podsumowanie zalecanych operatorów języka 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

Formatowanie wyrażeń operatorów zakresu

Dodaj spacje tylko wtedy .. , gdy wszystkie wyrażenia są niepodzielne. Liczby całkowite i pojedyncze identyfikatory wyrazów są uznawane za niepodzielne.

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

Te reguły dotyczą również fragmentowania:

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

Formatowanie wyrażeń if

Wcięcie warunkowe zależy od rozmiaru i złożoności wyrażeń, które je tworzą. Zapisz je w jednym wierszu, gdy:

  • cond, e1i e2 są krótkie.
  • e1 i e2 nie if/then/else są wyrażeniami samymi.
// ✔️ OK
if cond then e1 else e2

Jeśli wyrażenie else jest nieobecne, zaleca się, aby nigdy nie zapisywać całego wyrażenia w jednym wierszu. Jest to odróżnienie kodu imperatywnego od funkcjonalności.

// ✔️ OK
if a then
    ()

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

Jeśli dowolne z wyrażeń jest wielowierszowe, każda gałąź warunkowa powinna być wielowierszowa.

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

Wiele warunkowych elif elementów i else jest wcięć w tym samym zakresie co if w przypadku, gdy są zgodne z regułami jednego wyrażenia wiersza if/then/else .

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

Jeśli którykolwiek z warunków lub wyrażeń jest wielowierszowy, całe if/then/else wyrażenie jest wielowierszowe:

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

Jeśli warunek jest wielowierszowy lub przekracza tolerancję domyślną pojedynczego wiersza, wyrażenie warunku powinno używać jednego wcięcia i nowego wiersza. Słowo if kluczowe i then powinno być wyrównane podczas hermetyzacji wyrażenia długiego warunku.

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

Jest to jednak lepszy styl refaktoryzacji długich warunków do powiązania let lub oddzielnej funkcji:

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

if performAction then
    e1
else
    e2

Formatowanie wyrażeń wielkości liter unii

Stosowanie przypadków związków dyskryminowanych jest zgodne z tymi samymi regułami co aplikacje funkcji i metod. Oznacza to, że nazwa jest wielkich liter, moduły formatujące kod usuwają spację przed krotką:

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

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

Podobnie jak aplikacje funkcji, konstrukcje podzielone na wiele wierszy powinny używać wcięcia:

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

Formatowanie wyrażeń list i tablic

Zapis x :: l ze spacjami wokół :: operatora (:: jest operatorem przyrostka, dlatego otoczony spacjami).

Listy i tablice zadeklarowane w jednym wierszu powinny mieć spację po nawiasie otwierającym i przed nawiasem zamykającym:

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

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

Zawsze używaj co najmniej jednego odstępu między dwoma odrębnymi operatorami przypominającym nawiasy klamrowe. Na przykład pozostaw spację między wartością a a [{.

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

Te same wytyczne dotyczą list lub tablic krotki.

Listy i tablice podzielone na wiele wierszy są zgodne z podobną regułą jak rekordy:

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

Podobnie jak w przypadku rekordów, deklarowanie nawiasów otwierających i zamykających na własnym wierszu ułatwi przenoszenie kodu i potokowanie do funkcji:

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

Jeśli wyrażenie listy lub tablicy jest po prawej stronie powiązania, możesz użyć Stroustrup stylu:

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

Jeśli jednak wyrażenie listy lub tablicy nie jest prawą stroną powiązania, na przykład gdy znajduje się wewnątrz innej listy lub tablicy, jeśli to wyrażenie wewnętrzne musi zawierać wiele wierszy, nawiasy powinny znajdować się we własnych wierszach:

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

Ta sama reguła dotyczy typów rekordów wewnątrz tablic/list:

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

Podczas programowego generowania tablic i list preferuj ->do ... yield , gdy wartość jest zawsze generowana:

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

Starsze wersje języka F# wymagane do yield określenia w sytuacjach, w których dane mogą być generowane warunkowo lub mogą być obliczane kolejne wyrażenia. Preferuj pomijanie tych yield słów kluczowych, chyba że musisz skompilować starszą wersję języka F#:

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

W niektórych przypadkach do...yield może pomóc w czytelności. Należy wziąć pod uwagę te przypadki, choć subiektywne.

Formatowanie wyrażeń rekordów

Krótkie rekordy można zapisywać w jednym wierszu:

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

Rekordy, które są dłuższe, powinny używać nowych wierszy dla etykiet:

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

Style formatowania nawiasów wielowierszowych

W przypadku rekordów obejmujących wiele wierszy istnieją trzy powszechnie używane style formatowania: Cramped, Aligned, i Stroustrup. Styl Cramped był domyślnym stylem kodu języka F#, ponieważ ma tendencję do faworyzowania stylów, które umożliwiają kompilatorowi łatwe analizowanie kodu. Stroustrup Oba Aligned style umożliwiają łatwiejsze zmienianie kolejności elementów członkowskich, co może być łatwiejsze do refaktoryzacji, z wadą, że niektóre sytuacje mogą wymagać nieco bardziej szczegółowego kodu.

  • Cramped: historyczny standard i domyślny format rekordu języka F#. Nawiasy otwierające idą w tym samym wierszu co pierwszy element członkowski, zamykając nawias w tym samym wierszu co ostatni element członkowski.

    let rainbow = 
        { Boss1 = "Jeffrey"
          Boss2 = "Jeffrey"
          Boss3 = "Jeffrey"
          Lackeys = [ "Zippy"; "George"; "Bungle" ] }
    
  • Aligned: Każdy nawias uzyskuje własną linię wyrównaną do tej samej kolumny.

    let rainbow =
        {
            Boss1 = "Jeffrey"
            Boss2 = "Jeffrey"
            Boss3 = "Jeffrey"
            Lackeys = ["Zippy"; "George"; "Bungle"]
        }
    
  • Stroustrup: Nawias otwierający przechodzi w tym samym wierszu co powiązanie, nawias zamykający pobiera własną linię.

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

Te same reguły stylu formatowania dotyczą elementów listy i tablicy.

Formatowanie wyrażeń rekordów kopiowania i aktualizacji

Wyrażenie rekordu kopiowania i aktualizacji jest nadal rekordem, więc mają zastosowanie podobne wytyczne.

Wyrażenia krótkie mogą mieścić się w jednym wierszu:

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

Dłuższe wyrażenia powinny używać nowych wierszy i formatować na podstawie jednej z powyższych konwencji:

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

Uwaga: w przypadku używania Stroustrup stylu dla wyrażeń kopiowania i aktualizacji należy wciąć elementy członkowskie dalej niż nazwa skopiowanego rekordu:

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

Dopasowywanie wzorca formatowania

Użyj elementu dla | każdej klauzuli dopasowania bez wcięcia. Jeśli wyrażenie jest krótkie, możesz rozważyć użycie pojedynczego wiersza, jeśli każde wyrażenie podrzędne jest również proste.

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

Jeśli wyrażenie po prawej stronie strzałki dopasowania wzorca jest zbyt duże, przenieś je do następującego wiersza, wcięcie o jeden krok od match/|.

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

Podobnie jak w przypadku dużych warunków, jeśli wyrażenie dopasowania jest wielowierszowe lub przekracza domyślną tolerancję pojedynczego wiersza, wyrażenie dopasowania powinno używać jednego wcięcia i nowego wiersza. Słowo match kluczowe i with powinno być wyrównane podczas hermetyzacji długiego wyrażenia dopasowania.

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

Jednak lepszym stylem jest refaktoryzacja długich wyrażeń dopasowania do powiązania let lub oddzielnej funkcji:

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

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

Należy unikać wyrównania strzałek dopasowania wzorca.

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

Dopasowywanie wzorca wprowadzone przy użyciu słowa kluczowego function powinno spowodować wcięcie jednego poziomu od początku poprzedniego wiersza:

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

Użycie funkcji zdefiniowanych function przez let lub let rec powinno być ogólnie unikane na rzecz elementu match. W przypadku użycia reguły wzorca powinny być zgodne ze słowem kluczowym 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

Formatowanie try/with expressions

Dopasowanie wzorca dla typu wyjątku powinno być wcięcie na tym samym poziomie co 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"

Dodaj klauzulę | dla każdej klauzuli, z wyjątkiem sytuacji, gdy istnieje tylko jedna klauzula:

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

Formatowanie nazwanych argumentów

Nazwane argumenty powinny mieć spacje otaczające element =:

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

Podczas dopasowywania wzorców przy użyciu związków dyskryminowanych nazwane wzorce są formatowane podobnie, na przykład.

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

Formatowanie wyrażeń mutacji

Wyrażenia mutacji location <- expr są zwykle formatowane w jednym wierszu. Jeśli wymagane jest formatowanie wielowierszowe, umieść wyrażenie po prawej stronie w nowym wierszu.

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

Formatowanie wyrażeń obiektów

Elementy członkowskie wyrażeń obiektów powinny być wyrównane do member wcięcia o jeden poziom.

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

Możesz również użyć Stroustrup stylu:

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

Puste definicje typów mogą być formatowane w jednym wierszu:

type AnEmptyType = class end

Niezależnie od wybranej szerokości strony, = class end zawsze powinien znajdować się w tym samym wierszu.

Formatowanie wyrażeń indeksu/wycinka

Wyrażenia indeksu nie powinny zawierać żadnych spacji wokół nawiasów otwierających i zamykających.

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

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

Dotyczy to również starszej expr.[idx] składni.

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

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

Formatowanie wyrażeń cytowanych

Symbole ogranicznika (<@ , , @><@@, @@>) powinny być umieszczane w oddzielnych wierszach, jeśli wyrażenie cudzysłów jest wyrażeniem wielowierszowym.

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

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

W wyrażeniach jednowierszowych symbole ogranicznika powinny być umieszczane w tym samym wierszu co samo wyrażenie.

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

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

Formatowanie wyrażeń łańcuchowych

Gdy wyrażenia łańcuchowe (aplikacje funkcji połączone z .) są długie, umieść każde wywołanie aplikacji w następnym wierszu. Wcięcie kolejnych łączy w łańcuchu o jeden poziom po łączem wiodącym.

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

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

Link wiodący może składać się z wielu linków, jeśli są prostymi identyfikatorami. Na przykład dodanie w pełni kwalifikowanej przestrzeni nazw.

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

Kolejne linki powinny również zawierać proste identyfikatory.

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

Gdy argumenty wewnątrz aplikacji funkcji nie mieszczą się w pozostałej części wiersza, umieść każdy argument w następnym wierszu.

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

Argumenty lambda wewnątrz aplikacji funkcji powinny zaczynać się w tym samym wierszu co otwierający (element .

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

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

Deklaracje formatowania

W tej sekcji omówiono deklaracje formatowania różnych rodzajów.

Dodawanie pustych wierszy między deklaracjami

Oddzielaj definicje funkcji najwyższego poziomu i klas za pomocą pojedynczego pustego wiersza. Na przykład:

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

Jeśli konstrukcja zawiera komentarze doc XML, dodaj pusty wiersz przed komentarzem.

// ✔️ OK

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

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

Formatowanie deklaracji let i składowych

Podczas formatowania let i member deklaracji po prawej stronie powiązania występuje jeden wiersz lub (jeśli jest zbyt długi) przechodzi w nowy wiersz z wcięciem na jednym poziomie.

Na przykład następujące przykłady są zgodne:

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

Są one niezgodne:

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

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

Wystąpienia typów rekordów mogą również umieszczać nawiasy w własnych wierszach:

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

Możesz również użyć Stroustrup stylu z otwarciem { w tym samym wierszu co nazwa powiązania:

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

Oddzielaj elementy członkowskie pojedynczym pustym wierszem i dokumentem i dodaj komentarz do dokumentacji:

// ✔️ OK

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

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

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

Dodatkowe puste wiersze mogą być używane (oszczędnie) do oddzielania grup powiązanych funkcji. Puste wiersze mogą zostać pominięte między kilkoma powiązanymi wierszami jednowierszowymi (na przykład zestaw fikcyjnych implementacji). Używaj pustych wierszy w funkcjach, oszczędnie, aby wskazać sekcje logiczne.

Formatowanie argumentów funkcji i składowych

Podczas definiowania funkcji należy użyć odstępu wokół każdego argumentu.

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

Jeśli masz długą definicję funkcji, umieść parametry w nowych wierszach i wcięcie, aby były zgodne z poziomem wcięcia kolejnego parametru.

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

Dotyczy to również elementów członkowskich, konstruktorów i parametrów przy użyciu krotki:

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

Jeśli parametry są curried, umieść = znak wraz z dowolnym typem zwrotnym w nowym wierszu:

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

Jest to sposób, aby uniknąć zbyt długich wierszy (w przypadku zwracania typu może mieć długą nazwę) i mieć mniejsze uszkodzenia wiersza podczas dodawania parametrów.

Deklaracje operatorów formatowania

Opcjonalnie użyj białych znaków, aby otaczać definicję operatora:

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

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

W przypadku każdego operatora niestandardowego rozpoczynającego się od * i zawierającego więcej niż jeden znak należy dodać biały znak na początku definicji, aby uniknąć niejednoznaczności kompilatora. W związku z tym zalecamy po prostu otaczanie definicji wszystkich operatorów pojedynczym znakiem odstępu.

Formatowanie deklaracji rekordów

W przypadku deklaracji rekordów domyślnie należy wcięć { definicję typu według czterech spacji, uruchomić listę etykiet w tym samym wierszu i wyrównać elementy członkowskie, jeśli istnieją, przy użyciu tokenu { :

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

Często preferowane jest również umieszczanie nawiasów kwadratowych na własnym wierszu z etykietami wciętymi przez dodatkowe cztery spacje:

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

Możesz również umieścić element { na końcu pierwszego wiersza definicji typu (Stroustrup styl):

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

Jeśli są potrzebne dodatkowe elementy członkowskie, nie używaj with/end ich zawsze, gdy jest to możliwe:

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

Wyjątkiem od tej reguły stylu jest formatowanie rekordów zgodnie z stylem Stroustrup . W takiej sytuacji ze względu na reguły kompilatora with słowo kluczowe jest wymagane, jeśli chcesz zaimplementować interfejs lub dodać dodatkowe elementy członkowskie:

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

W przypadku dodania dokumentacji XML dla pól rekordów Aligned lub Stroustrup preferowanego stylu należy dodać dodatkowe odstępy między elementami członkowskimi:

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

Umieszczenie tokenu otwierającego w nowym wierszu i token zamykający w nowym wierszu jest preferowane, jeśli deklarujesz implementacje interfejsu lub elementy członkowskie rekordu:

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

Te same reguły dotyczą aliasów typu anonimowego rekordu.

Formatowanie deklaracji unii dyskryminowanej

W przypadku deklaracji unii dyskryminowanej wcięcie | w definicji typu o cztery spacje:

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

Jeśli istnieje jedna krótka unia, można pominąć wiodące |.

// ✔️ OK
type Address = Address of string

W przypadku dłuższej lub wielowierszowej unii zachowaj | pole i umieść każde pole unii na nowym wierszu, oddzielając * je na końcu każdego wiersza.

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

Po dodaniu komentarzy do dokumentacji użyj pustego wiersza przed każdym /// komentarzem.

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

Formatowanie deklaracji literałów

Literały języka F# używające atrybutu Literal powinny umieszczać atrybut we własnym wierszu i używać nazewnictwa PascalCase:

// ✔️ OK

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

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

Unikaj umieszczania atrybutu w tym samym wierszu co wartość.

Formatowanie deklaracji modułów

Kod w module lokalnym musi być wcięte względem modułu, ale kod w module najwyższego poziomu nie powinien być wcięcie. Elementy przestrzeni nazw nie muszą być wcięcia.

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

Formatowanie deklaracji do

W deklaracjach typów deklaracje modułów i wyrażenia obliczeniowe użycie do lub do! jest czasami wymagane do wykonywania operacji ubocznych. Gdy obejmują one wiele wierszy, użyj wcięcia i nowego wiersza, aby zachować wcięcie spójne z let/let!. Oto przykład użycia do w klasie:

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

Oto przykład użycia do! dwóch spacji wcięcia (ponieważ w przypadku do! braku przypadkowej różnicy między podejściami w przypadku używania czterech spacji wcięcia):

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

Formatowanie operacji wyrażeń obliczeniowych

Podczas tworzenia operacji niestandardowych dla wyrażeń obliczeniowych zaleca się używanie nazewnictwa 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
    }

Domena, która jest modelowana, powinna ostatecznie napędzać konwencję nazewnictwa. Jeśli idiomatyczne jest użycie innej konwencji, należy zamiast tego użyć tej konwencji.

Jeśli wartość zwracana wyrażenia jest wyrażeniem obliczeniowym, preferuj umieszczenie nazwy słowa kluczowego wyrażenia obliczeniowego na własnym wierszu:

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

Możesz również wolisz umieścić wyrażenie obliczeniowe w tym samym wierszu co nazwa powiązania:

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

Niezależnie od preferencji, należy dążyć do zachowania spójności w całej bazie kodu. Formatery mogą zezwalać na określenie tej preferencji, aby zachować spójność.

Typy formatowania i adnotacje typów

W tej sekcji omówiono typy formatowania i adnotacje typów. Obejmuje to formatowanie plików podpisów z .fsi rozszerzeniem .

W przypadku typów preferuj składnię prefiksu dla typów ogólnych (Foo<T>) z określonymi wyjątkami

Język F# umożliwia zarówno styl postfiksu pisania typów ogólnych (na przykład int list) i stylu prefiksu (na przykład list<int>). Styl postfiksu może być używany tylko z pojedynczym argumentem typu. Zawsze preferuj styl platformy .NET, z wyjątkiem pięciu określonych typów:

  1. W przypadku list języka F# użyj formularza postfiksu: int list zamiast list<int>.
  2. W przypadku opcji języka F# użyj formularza postfiksu: int option zamiast option<int>.
  3. W przypadku opcji wartości języka F# użyj formularza postfiksu: int voption zamiast voption<int>.
  4. W przypadku tablic języka F# użyj formularza postfiksu: int array zamiast array<int> lub int[].
  5. W przypadku komórek odwołań użyj int ref zamiast ref<int> lub Ref<int>.

W przypadku wszystkich innych typów użyj formularza prefiksu.

Typy funkcji formatowania

Podczas definiowania podpisu funkcji użyj białych znaków wokół symbolu -> :

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

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

Formatowanie wartości i adnotacji typu argumentu

Podczas definiowania wartości lub argumentów z adnotacjami typu użyj odstępu po symbolu : , ale nie przed:

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

Formatowanie adnotacji typu wielowierszowego

Gdy adnotacja typu jest długa lub wielowierszowa, umieść je w następnym wierszu, wcięcie o jeden poziom.

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

W przypadku wbudowanych typów rekordów anonimowych można również użyć Stroustrup stylu:

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

Formatowanie adnotacji zwracanych typów

W adnotacjach funkcji lub elementu członkowskiego zwracanego typu użyj białych znaków przed symbolem i po nim : :

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

Typy formatowania w podpisach

Podczas pisania pełnych typów funkcji w podpisach czasami konieczne jest podzielenie argumentów na wiele wierszy. Zwracany typ jest zawsze wcięcie.

W przypadku funkcji tupled argumenty są rozdzielane znakami *, umieszczonymi na końcu każdego wiersza.

Rozważmy na przykład funkcję z następującą implementacją:

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

W odpowiednim pliku podpisu (.fsi rozszerzenie) funkcję można sformatować w następujący sposób, gdy wymagane jest formatowanie wielowierszowe:

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

Podobnie należy wziąć pod uwagę funkcję curried:

let SampleCurriedFunction arg1 arg2 arg3 arg4 = ...

W odpowiednim pliku podpisu element -> jest umieszczany na końcu każdego wiersza:

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

Podobnie rozważ funkcję, która przyjmuje kombinację argumentów curried i tupled:

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

W odpowiednim pliku podpisu typy poprzedzone krotką są wcięty

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

Te same reguły dotyczą elementów członkowskich w podpisach typów:

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

Formatowanie jawnych argumentów i ograniczeń typu ogólnego

Poniższe wytyczne dotyczą definicji funkcji, definicji składowych, definicji typów i aplikacji funkcji.

Zachowaj argumenty typów ogólnych i ograniczenia w jednym wierszu, jeśli nie jest zbyt długi:

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

Jeśli oba argumenty typu ogólnego/ograniczenia i parametry funkcji nie pasują, ale parametry typu/ograniczenia są same, umieść parametry w nowych wierszach:

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

Jeśli parametry lub ograniczenia typu są zbyt długie, przerwij je i wyrównaj, jak pokazano poniżej. Zachowaj listę parametrów typu w tym samym wierszu co funkcja, niezależnie od jego długości. W przypadku ograniczeń umieść when w pierwszym wierszu i zachowaj każde ograniczenie w jednym wierszu niezależnie od jego długości. Umieść > na końcu ostatniego wiersza. Wcięcie ograniczeń o jeden poziom.

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

Jeśli parametry/ograniczenia typu są podzielone, ale nie ma normalnych parametrów funkcji, umieść element = w nowym wierszu niezależnie od tego:

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

Te same reguły dotyczą aplikacji funkcji:

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

Formatowanie dziedziczenia

Argumenty konstruktora klasy bazowej są wyświetlane na liście argumentów w klauzuli inherit . Umieść klauzulę inherit w nowym wierszu z wcięciem o jeden poziom.

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)

Gdy konstruktor jest długi lub wielowierszowy, umieść je w następnym wierszu, wcięcie o jeden poziom.
Sformatuj ten wielowierszowy konstruktor zgodnie z regułami aplikacji funkcji wielowierszowych.

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

Formatowanie konstruktora podstawowego

W domyślnych konwencjach formatowania żadne miejsce nie jest dodawane między nazwą typu a nawiasami dla konstruktora podstawowego.

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

Wiele konstruktorów

Jeśli klauzula inherit jest częścią rekordu, umieść ją w tym samym wierszu, jeśli jest krótka. Umieść go w następnym wierszu, wcięcie o jeden poziom, jeśli jest długi lub wielowierszowy.

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 }

Atrybuty formatowania

Atrybuty są umieszczane nad konstrukcją:

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

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

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

Powinny one przejść po dowolnej dokumentacji XML:

// ✔️ OK

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

Formatowanie atrybutów parametrów

Atrybuty można również umieścić na parametrach. W takim przypadku umieść polecenie w tym samym wierszu co parametr i przed nazwą:

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

Formatowanie wielu atrybutów

W przypadku zastosowania wielu atrybutów do konstrukcji, która nie jest parametrem, umieść każdy atrybut w osobnym wierszu:

// ✔️ OK

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

Po zastosowaniu do parametru umieść atrybuty w tym samym wierszu i rozdziel je separatorem ; .

Podziękowania

Te wytyczne są oparte na kompleksowym przewodniku po konwencji formatowania języka F# autorstwa Anh-Dung Phan.