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
,e1
ie2
są krótkie.e1
ie2
nieif/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 osobnych wierszach, @>
jeśli wyrażenie cytowane 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:
- W przypadku list języka F# użyj formularza postfiksu:
int list
zamiastlist<int>
. - W przypadku opcji języka F# użyj formularza postfiksu:
int option
zamiastoption<int>
. - W przypadku opcji wartości języka F# użyj formularza postfiksu:
int voption
zamiastvoption<int>
. - W przypadku tablic języka F# użyj formularza postfiksu:
int array
zamiastarray<int>
lubint[]
. - W przypadku komórek odwołań użyj
int ref
zamiastref<int>
lubRef<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.