Linee guida per la formattazione del codice F#

Questo articolo offre linee guida per la formattazione del codice in modo che il codice F# sia:

  • Più leggibile
  • In base alle convenzioni applicate tramite gli strumenti di formattazione in Visual Studio Code e altri editor
  • Simile ad altro codice online

Vedere anche Convenzioni di codifica e Linee guida per la progettazione dei componenti, che illustrano anche le convenzioni di denominazione.

Formattazione automatica del codice

Il formattatore di codice Fantomas è lo strumento standard della community F# per la formattazione automatica del codice. Le impostazioni predefinite corrispondono a questa guida di stile.

È consigliabile usare questo formattatore di codice. All'interno dei team F# le specifiche di formattazione del codice devono essere concordate e codificate in termini di un file di impostazioni concordato per il formattatore di codice archiviato nel repository del team.

Regole generali per la formattazione

F# usa spazi vuoti significativi per impostazione predefinita ed è sensibile agli spazi vuoti. Le linee guida seguenti sono concepite per fornire indicazioni su come destreggiarsi su alcune sfide che possono imporre.

Usare spazi non schede

Quando è necessario il rientro, è necessario usare spazi, non schede. Il codice F# non usa schede e il compilatore genererà un errore se viene rilevato un carattere di tabulazione all'esterno di un valore letterale stringa o di un commento.

Usare un rientro coerente

Quando si rientra, è necessario almeno uno spazio. L'organizzazione può creare standard di codifica per specificare il numero di spazi da usare per il rientro; due, tre o quattro spazi di rientro a ogni livello in cui si verifica il rientro è tipico.

È consigliabile usare quattro spazi per rientro.

Detto questo, il rientro dei programmi è una questione soggettiva. Le varianti sono OK, ma la prima regola da seguire è la coerenza del rientro. Scegliere uno stile di rientro generalmente accettato e usarlo sistematicamente in tutta la codebase.

Evitare la formattazione sensibile alla lunghezza del nome

Cercare di evitare il rientro e l'allineamento sensibili alla denominazione:

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

I motivi principali per evitare questo problema sono:

  • Il codice importante viene spostato a destra
  • È stata lasciata una larghezza inferiore per il codice effettivo
  • La ridenominazione può interrompere l'allineamento

Evitare spazi vuoti estranei

Evitare spazi vuoti estranei nel codice F#, tranne dove descritto in questa guida di stile.

// ✔️ OK
spam (ham 1)

// ❌ Not OK
spam ( ham 1 )

Formattazione dei commenti

Preferisce più commenti a barra doppia rispetto ai commenti di blocco.

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

I commenti devono maiuscolare la prima lettera e essere frasi o frasi ben formate.

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

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

Per la formattazione dei commenti della documentazione XML, vedere "Dichiarazioni di formattazione" di seguito.

Formattazione di espressioni

In questa sezione vengono illustrate le espressioni di formattazione di tipi diversi.

Formattazione di espressioni stringa

I valori letterali stringa e le stringhe interpolate possono essere lasciati su una singola riga, indipendentemente dalla durata della riga.

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

Le espressioni interpolate a più righe sono sconsigliate. Associare invece il risultato dell'espressione a un valore e usarlo nella stringa interpolata.

Formattazione delle espressioni di tupla

Una creazione di un'istanza di tupla deve essere racchiusa tra parentesi e le virgole delimitatori all'interno devono essere seguite da un singolo spazio, ad esempio : (1, 2), (x, y, z).

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

È comunemente accettato di omettere parentesi nei criteri di ricerca delle tuple:

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

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

È anche comunemente accettato di omettere parentesi se la tupla è il valore restituito di una funzione:

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

In sintesi, preferisce le istanze delle tuple racchiuse tra parentesi, ma quando si usano tuple per la corrispondenza dei criteri o un valore restituito, è considerato corretto evitare parentesi.

Formattazione delle espressioni dell'applicazione

Quando si formatta un'applicazione di funzione o metodo, gli argomenti vengono forniti nella stessa riga quando la larghezza della riga consente:

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

Omettere le parentesi a meno che gli argomenti non richiedano:

// ✔️ OK
someFunction1 x.IngredientName

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

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

Non omettere gli spazi quando si richiamano con più argomenti 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)

Nelle convenzioni di formattazione predefinite, viene aggiunto uno spazio quando si applicano funzioni con lettere minuscole agli argomenti tupled o racchiusi tra parentesi (anche quando viene usato un singolo argomento):

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

Nelle convenzioni di formattazione predefinite non viene aggiunto alcuno spazio quando si applicano metodi in maiuscolo agli argomenti tupled. Ciò è dovuto al fatto che vengono spesso usati con la programmazione Fluent:

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

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

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

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

Potrebbe essere necessario passare argomenti a una funzione su una nuova riga come una questione di leggibilità o perché l'elenco di argomenti o i nomi degli argomenti sono troppo lunghi. In tal caso, impostare un rientro di un livello:

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

Quando la funzione accetta un singolo argomento tupled su più righe, posizionare ogni argomento su una nuova riga:

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

Se le espressioni di argomento sono brevi, separare gli argomenti con spazi e mantenerli in una riga.

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

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

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

Se le espressioni di argomento sono lunghe, usare le righe nuove e impostare il rientro di un livello, anziché rientrare tra parentesi a sinistra.

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

Le stesse regole si applicano anche se è presente un solo argomento su più righe, incluse le stringhe a più righe:

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

Formattazione delle espressioni della pipeline

Quando si usano più righe, gli operatori della pipeline |> devono entrare sotto le espressioni su cui operano.

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

Formattazione di espressioni lambda

Quando un'espressione lambda viene usata come argomento in un'espressione a più righe e seguita da altri argomenti, posizionare il corpo di un'espressione lambda su una nuova riga, rientrato in base a un livello:

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

Se l'argomento lambda è l'ultimo argomento in un'applicazione di funzione, posizionare tutti gli argomenti fino alla freccia sulla stessa riga.

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

Trattare le corrispondenze lambda in modo simile.

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

Quando sono presenti molti argomenti iniziali o su più righe prima del rientro lambda di tutti gli argomenti con un livello.

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

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

Se il corpo di un'espressione lambda è lungo più righe, è consigliabile effettuare il refactoring in una funzione con ambito locale.

Quando le pipeline includono espressioni lambda, ogni espressione lambda è in genere l'ultimo argomento in ogni fase della pipeline:

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

Nel caso in cui gli argomenti di un'espressione lambda non si adattino a una singola riga o siano su più righe, inserirli nella riga successiva, rientrati di un livello.

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

Formattazione di espressioni aritmetiche ed binarie

Usare sempre spazi vuoti intorno alle espressioni aritmetiche binarie:

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

L'errore di racchiudere un operatore binario - , se combinato con determinate scelte di formattazione, potrebbe comportare l'interpretazione di un operatore unario -. Gli operatori unari - devono essere sempre seguiti immediatamente dal valore che negano:

// ✔️ OK
let negate x = -x

// ❌ Not OK
let negateBad x = - x

L'aggiunta di uno spazio vuoto dopo l'operatore - può causare confusione per altri utenti.

Separare gli operatori binari per spazi. Le espressioni di prefisso sono OK per la riga nella stessa colonna:

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

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

Questa regola si applica anche alle unità di misura in tipi e annotazioni costanti:

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

Gli operatori seguenti sono definiti nella libreria standard F# e devono essere usati invece di definire equivalenti. L'uso di questi operatori è consigliato perché tende a rendere il codice più leggibile e idiomatico. L'elenco seguente riepiloga gli operatori F# consigliati.

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

Formattazione delle espressioni dell'operatore di intervallo

Aggiungere spazi intorno a .. quando tutte le espressioni non sono atomiche. I numeri interi e gli identificatori di parola singola sono considerati atomici.

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

Queste regole si applicano anche al sezionamento:

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

Formattazione di espressioni if

Il rientro delle condizionali dipende dalle dimensioni e dalla complessità delle espressioni che li compongono. Scriverli su una riga quando:

  • cond, e1e e2 sono brevi.
  • e1 e e2 non if/then/else sono espressioni stesse.
// ✔️ OK
if cond then e1 else e2

Se l'espressione else è assente, è consigliabile non scrivere mai l'intera espressione in una riga. Si tratta di distinguere il codice imperativo dalla funzionalità.

// ✔️ OK
if a then
    ()

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

Se una qualsiasi delle espressioni è multilinea, ogni ramo condizionale deve essere multilinea.

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

Più condizionali con elif e else sono rientrati nello stesso ambito di if quando seguono le regole delle espressioni di una riga if/then/else .

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

Se una delle condizioni o delle espressioni è multilinea, l'intera if/then/else espressione è su più righe:

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

Se una condizione è multilinea o supera la tolleranza predefinita della riga singola, l'espressione della condizione deve usare un rientro e una nuova riga. La if parola chiave e then deve essere allineata durante l'incapsulamento dell'espressione di condizione long.

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

È tuttavia preferibile eseguire il refactoring di condizioni lunghe per un'associazione di tipo let o una funzione separata:

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

if performAction then
    e1
else
    e2

Formattazione delle espressioni di maiuscole e minuscole dell'unione

L'applicazione di casi di unione discriminati segue le stesse regole delle applicazioni di funzioni e metodi. Ovvero, poiché il nome è in maiuscolo, i formattatori di codice rimuoveranno uno spazio prima di una tupla:

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

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

Analogamente alle applicazioni per le funzioni, le costruzioni suddivise tra più righe devono usare il rientro:

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

Formattazione di espressioni di elenco ed array

Scrivere x :: l con spazi intorno all'operatore :: (:: è un operatore di prefisso, quindi racchiuso da spazi).

L'elenco e le matrici dichiarate su una singola riga devono avere uno spazio dopo la parentesi aperta e prima della parentesi chiusa:

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

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

Usare sempre almeno uno spazio tra due operatori distinti simili a parentesi graffe. Ad esempio, lasciare uno spazio tra un [ oggetto e un oggetto {.

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

Le stesse linee guida si applicano agli elenchi o alle matrici di tuple.

Gli elenchi e le matrici che si dividono tra più righe seguono una regola simile a quella dei record:

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

Come per i record, dichiarando le parentesi di apertura e chiusura sulla propria riga, il codice di spostamento e il piping nelle funzioni semplificano:

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

Se un'espressione di elenco o matrice è il lato destro di un'associazione, è consigliabile usare Stroustrup lo stile:

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

Tuttavia, quando un elenco o un'espressione di matrice non è il lato destro di un'associazione, ad esempio quando si trova all'interno di un altro elenco o matrice, se tale espressione interna deve estendersi su più righe, le parentesi quadre devono andare sulle proprie righe:

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

La stessa regola si applica ai tipi di record all'interno di matrici/elenchi:

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

Quando si generano matrici ed elenchi a livello di codice, preferire -> quando do ... yield viene sempre generato un valore:

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

Nelle versioni precedenti di F# è necessario specificare yield in situazioni in cui i dati possono essere generati in modo condizionale oppure possono essere valutate espressioni consecutive. Preferire omettere queste yield parole chiave a meno che non sia necessario compilare con una versione precedente del linguaggio 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"
    ]

In alcuni casi, do...yield può essere utile per la leggibilità. Questi casi, anche se soggettivi, devono essere presi in considerazione.

Formattazione delle espressioni di record

I record brevi possono essere scritti in una sola riga:

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

I record più lunghi devono usare nuove righe per le etichette:

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

Stili di formattazione tra parentesi quadre su più righe

Per i record che si estendono su più righe, esistono tre stili di formattazione comunemente usati: Cramped, Alignede Stroustrup. Lo Cramped stile è stato lo stile predefinito per il codice F#, poiché tende a favorire gli stili che consentono al compilatore di analizzare facilmente il codice. Entrambi Aligned gli stili e Stroustrup consentono di riordinare più facilmente i membri, portando a codice che potrebbe essere più facile da effettuare il refactoring, con lo svantaggio che alcune situazioni possono richiedere codice leggermente più dettagliato.

  • Cramped: formato di record F# cronologico e predefinito. Le parentesi quadre di apertura vanno sulla stessa riga del primo membro, con parentesi quadre chiuse sulla stessa riga dell'ultimo membro.

    let rainbow = 
        { Boss1 = "Jeffrey"
          Boss2 = "Jeffrey"
          Boss3 = "Jeffrey"
          Lackeys = [ "Zippy"; "George"; "Bungle" ] }
    
  • Aligned: le parentesi quadre ottengono la propria linea, allineate sulla stessa colonna.

    let rainbow =
        {
            Boss1 = "Jeffrey"
            Boss2 = "Jeffrey"
            Boss3 = "Jeffrey"
            Lackeys = ["Zippy"; "George"; "Bungle"]
        }
    
  • Stroustrup: la parentesi quadra aperta va sulla stessa riga dell'associazione, la parentesi chiusa ottiene la propria riga.

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

Le stesse regole di stile di formattazione si applicano per gli elementi elenco e matrice.

Formattazione delle espressioni di record di copia e aggiornamento

Un'espressione di record di copia e aggiornamento è ancora un record, quindi si applicano linee guida simili.

Le espressioni brevi possono essere adattate a una sola riga:

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

Le espressioni più lunghe devono usare nuove righe e formattare in base a una delle convenzioni indicate in precedenza:

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

Nota: se si usa Stroustrup lo stile per le espressioni di copia e aggiornamento, è necessario impostare un rientro dei membri oltre il nome del record copiato:

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

Formattazione dei criteri di ricerca

Utilizzare per | ogni clausola di una corrispondenza senza rientro. Se l'espressione è breve, è possibile prendere in considerazione l'uso di una singola riga se ogni sottoespressione è anche semplice.

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

Se l'espressione a destra della freccia corrispondente al criterio è troppo grande, spostarla nella riga seguente, rientrando un passaggio dall'oggetto match/|.

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

Analogamente alle condizioni di grandi dimensioni, se un'espressione di corrispondenza è multilinea o supera la tolleranza predefinita della riga singola, l'espressione di corrispondenza deve usare un rientro e una nuova riga. La match parola chiave e with deve essere allineata durante l'incapsulamento dell'espressione di corrispondenza lunga.

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

È tuttavia preferibile eseguire il refactoring di espressioni di corrispondenza lunghe a una funzione let binding o separata:

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

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

È consigliabile evitare l'allineamento delle frecce di una corrispondenza del criterio.

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

I criteri di ricerca introdotti usando la parola chiave function devono rientrare un livello dall'inizio della riga precedente:

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

L'uso di function nelle funzioni definite da let o let rec deve essere in generale evitato a favore di un oggetto match. Se usato, le regole del criterio devono essere allineate con la parola chiave 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

Formattazione di espressioni try/with

La corrispondenza dei criteri nel tipo di eccezione deve essere rientrata allo stesso livello di 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"

Aggiungere un | per ogni clausola, tranne quando è presente una sola clausola:

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

Formattazione di argomenti denominati

Gli argomenti denominati devono avere spazi che circondano :=

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

Quando i criteri di ricerca usano unioni discriminate, i modelli denominati vengono formattati in modo analogo, ad esempio.

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

Formattazione delle espressioni di mutazione

Le espressioni di mutazione vengono in genere formattate location <- expr su una riga. Se è necessaria la formattazione su più righe, posizionare l'espressione sul lato destro su una nuova riga.

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

Formattazione delle espressioni di oggetto

I membri dell'espressione oggetto devono essere allineati al member rientro di un livello.

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

È anche consigliabile usare Stroustrup lo stile:

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

Le definizioni dei tipi vuoti possono essere formattate in una sola riga:

type AnEmptyType = class end

Indipendentemente dalla larghezza della pagina scelta, = class end deve essere sempre nella stessa riga.

Formattazione di espressioni di indice/sezione

Le espressioni di indice non devono contenere spazi intorno alle parentesi di apertura e di chiusura.

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

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

Questo vale anche per la sintassi precedente expr.[idx] .

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

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

Formattazione di espressioni tra virgolette

I simboli del delimitatore (<@ , @><@@, , @@>) devono essere posizionati su righe separate se l'espressione tra virgolette è un'espressione a più righe.

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

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

Nelle espressioni a riga singola i simboli del delimitatore devono essere posizionati sulla stessa riga dell'espressione stessa.

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

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

Formattazione delle espressioni concatenati

Quando le espressioni concatenati (applicazioni di funzione intrecciate con .) sono lunghe, inserire ogni chiamata dell'applicazione alla riga successiva. Rientrare i collegamenti successivi nella catena di un livello dopo il collegamento iniziale.

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

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

Il collegamento iniziale può essere composto da più collegamenti se sono identificatori semplici. Ad esempio, l'aggiunta di uno spazio dei nomi completo.

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

I collegamenti successivi devono contenere anche identificatori semplici.

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

Quando gli argomenti all'interno di un'applicazione di funzione non rientrano nel resto della riga, inserire ogni argomento nella riga successiva.

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

Gli argomenti lambda all'interno di un'applicazione di funzione devono iniziare nella stessa riga dell'apertura (di .

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

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

Dichiarazioni di formattazione

In questa sezione vengono illustrate le dichiarazioni di formattazione di tipi diversi.

Aggiungere righe vuote tra dichiarazioni

Separare le definizioni di classe e funzione di primo livello con una singola riga vuota. Ad esempio:

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

Se un costrutto contiene commenti di documenti XML, aggiungere una riga vuota prima del commento.

// ✔️ OK

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

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

Formattazione di dichiarazioni let e membro

Durante la formattazione let e member le dichiarazioni, in genere il lato destro di un'associazione passa su una riga o (se è troppo lungo) passa a un nuovo livello rientrato di una riga.

Ad esempio, gli esempi seguenti sono conformi:

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

Questi sono non conformi:

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

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

Le istanze dei tipi di record possono anche posizionare le parentesi quadre sulle proprie righe:

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

È anche possibile usare Stroustrup lo stile, con l'apertura { sulla stessa riga del nome dell'associazione:

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

Separare i membri con una singola riga vuota e un documento e aggiungere un commento alla documentazione:

// ✔️ OK

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

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

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

È possibile usare righe vuote aggiuntive (con moderazione) per separare gruppi di funzioni correlate. Le righe vuote possono essere omesse tra un gruppo di righe correlate ,ad esempio un set di implementazioni fittizie. Usare righe vuote nelle funzioni, con moderazione, per indicare sezioni logiche.

Formattazione di argomenti di funzione e membro

Quando si definisce una funzione, usare spazi vuoti intorno a ogni argomento.

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

Se si dispone di una definizione di funzione lunga, posizionare i parametri sulle nuove righe e impostare il rientro in modo che corrispondano al livello di rientro del parametro successivo.

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

Questo vale anche per membri, costruttori e parametri che usano tuple:

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

Se i parametri sono curried, posizionare il = carattere insieme a qualsiasi tipo restituito su una nuova riga:

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

Questo è un modo per evitare righe troppo lunghe (nel caso in cui il tipo restituito potrebbe avere un nome lungo) e avere meno danni alla riga durante l'aggiunta di parametri.

Dichiarazioni di operatore di formattazione

Facoltativamente, usare lo spazio vuoto per racchiudere una definizione di operatore:

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

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

Per qualsiasi operatore personalizzato che inizia con * e con più di un carattere, è necessario aggiungere uno spazio vuoto all'inizio della definizione per evitare un'ambiguità del compilatore. Per questo motivo, è consigliabile racchiudere semplicemente le definizioni di tutti gli operatori con un singolo carattere di spazio vuoto.

Formattazione delle dichiarazioni di record

Per le dichiarazioni di record, per impostazione predefinita è consigliabile impostare come rientro nella { definizione del tipo in quattro spazi, avviare l'elenco di etichette nella stessa riga e allineare i membri, se presenti, con il { token:

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

È anche comune preferire l'inserimento di parentesi quadre sulla propria riga, con etichette rientrate da quattro spazi aggiuntivi:

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

È anche possibile inserire l'oggetto { alla fine della prima riga della definizione del tipo (Stroustrup stile):

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

Se sono necessari membri aggiuntivi, non usare with/end quando possibile:

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

L'eccezione a questa regola di stile è se si formattano i record in base allo Stroustrup stile. In questo caso, a causa delle regole del compilatore, la with parola chiave è necessaria se si vuole implementare un'interfaccia o aggiungere altri membri:

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

Quando viene aggiunta la documentazione XML per i campi Aligned di record o Stroustrup lo stile è preferibile e tra i membri devono essere aggiunti spazi vuoti aggiuntivi:

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

Posizionare il token di apertura su una nuova riga e il token di chiusura su una nuova riga è preferibile se si dichiarano implementazioni o membri dell'interfaccia nel record:

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

Queste stesse regole si applicano agli alias dei tipi di record anonimi.

Formattazione di dichiarazioni di unione discriminate

Per le dichiarazioni di unione discriminate, rientrare | nella definizione del tipo in base a quattro spazi:

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

Quando è presente una singola unione breve, è possibile omettere l'oggetto iniziale |.

// ✔️ OK
type Address = Address of string

Per un'unione più lunga o su più righe, mantenere e | posizionare ogni campo di unione su una nuova riga, con la separazione * alla fine di ogni riga.

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

Quando vengono aggiunti i commenti della documentazione, usare una riga vuota prima di ogni /// commento.

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

Formattazione di dichiarazioni letterali

I valori letterali F# che usano l'attributo Literal devono inserire l'attributo nella propria riga e usare la denominazione PascalCase:

// ✔️ OK

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

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

Evitare di posizionare l'attributo sulla stessa riga del valore.

Dichiarazioni di modulo di formattazione

Il codice in un modulo locale deve essere rientrato rispetto al modulo, ma il codice in un modulo di primo livello non deve essere rientrato. Gli elementi dello spazio dei nomi non devono essere rientrati.

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

Formattazione delle dichiarazioni do

Nelle dichiarazioni di tipo, le dichiarazioni di modulo e le espressioni di calcolo, l'uso di do o è do! talvolta necessario per le operazioni di effetto collaterale. Quando si estendono su più righe, usare il rientro e una nuova riga per mantenere il rientro coerente con let/let!. Ecco un esempio che usa do in una classe:

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

Di seguito è riportato un esempio con do! l'uso di due spazi di rientro (perché con do! non esiste una differenza casuale tra gli approcci quando si usano quattro spazi di rientro):

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

Formattazione delle operazioni di espressione di calcolo

Quando si creano operazioni personalizzate per le espressioni di calcolo, è consigliabile usare la denominazione 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
    }

Il dominio modellato dovrebbe infine guidare la convenzione di denominazione. Se è idiomatico usare una convenzione diversa, è consigliabile usare tale convenzione.

Se il valore restituito di un'espressione è un'espressione di calcolo, è preferibile inserire il nome della parola chiave dell'espressione di calcolo nella propria riga:

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

È anche consigliabile inserire l'espressione di calcolo nella stessa riga del nome dell'associazione:

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

Indipendentemente dalla preferenza, è consigliabile mantenere la coerenza in tutta la codebase. I formattatori possono consentire di specificare questa preferenza per rimanere coerenti.

Formattazione di tipi e annotazioni di tipo

In questa sezione vengono illustrati i tipi di formattazione e le annotazioni dei tipi. Ciò include la formattazione dei file di firma con l'estensione .fsi .

Per i tipi, preferire la sintassi del prefisso per i generics (Foo<T>), con alcune eccezioni specifiche

F# consente sia lo stile di scrittura di tipi generici (ad esempio, int list) che lo stile del prefisso (ad esempio, list<int>). Lo stile di postfissi può essere usato solo con un singolo argomento di tipo. Preferisce sempre lo stile .NET, ad eccezione di cinque tipi specifici:

  1. Per gli elenchi F# usare il formato di prefisso: int list anziché list<int>.
  2. Per Le opzioni F# usare il formato di prefisso: int option anziché option<int>.
  3. Per Opzioni valore F#, usare il formato di prefisso: int voption anziché voption<int>.
  4. Per le matrici F#, usare il formato di prefisso: int array anziché array<int> o int[].
  5. Per le celle di riferimento, usare int ref anziché ref<int> o Ref<int>.

Per tutti gli altri tipi, usare il formato prefisso.

Formattazione dei tipi di funzione

Quando si definisce la firma di una funzione, usare spazi vuoti intorno al -> simbolo:

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

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

Formattazione di annotazioni di valori e tipi di argomento

Quando si definiscono valori o argomenti con annotazioni di tipo, usare spazi vuoti dopo il : simbolo, ma non prima:

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

Formattazione di annotazioni di tipi multilinea

Quando un'annotazione di tipo è lunga o multilinea, inserirle nella riga successiva, rientrate di un livello.

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

Per i tipi di record anonimi inline, è anche possibile usare Stroustrup lo stile:

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

Formattazione delle annotazioni dei tipi restituiti

Nelle annotazioni di tipo restituito di funzione o membro usare spazi vuoti prima e dopo il : simbolo:

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

Formattazione dei tipi nelle firme

Quando si scrivono tipi di funzione completi nelle firme, a volte è necessario suddividere gli argomenti su più righe. Il tipo restituito è sempre rientrato.

Per una funzione tupled, gli argomenti sono separati da *, posizionati alla fine di ogni riga.

Si consideri ad esempio una funzione con l'implementazione seguente:

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

Nel file di firma corrispondente (.fsi estensione) la funzione può essere formattata come segue quando è necessaria la formattazione a più righe:

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

Si consideri analogamente una funzione curried:

let SampleCurriedFunction arg1 arg2 arg3 arg4 = ...

Nel file di firma corrispondente, l'oggetto -> viene posizionato alla fine di ogni riga:

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

Analogamente, si consideri una funzione che accetta una combinazione di argomenti curried e tupled:

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

Nel file di firma corrispondente i tipi preceduti da una tupla sono rientrati

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

Le stesse regole si applicano ai membri nelle firme di tipo:

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

Formattazione di argomenti e vincoli di tipo generico espliciti

Le linee guida seguenti si applicano alle definizioni di funzione, alle definizioni dei membri, alle definizioni dei tipi e alle applicazioni per le funzioni.

Mantenere argomenti di tipo generico e vincoli su una singola riga se non è troppo lungo:

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

Se entrambi gli argomenti o i vincoli di tipo generico e i parametri di funzione non sono adatti, ma i parametri di tipo o i vincoli si applicano da soli, posizionare i parametri nelle nuove righe:

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

Se i parametri di tipo o i vincoli sono troppo lunghi, interromperli e allinearli come illustrato di seguito. Mantenere l'elenco dei parametri di tipo nella stessa riga della funzione, indipendentemente dalla lunghezza. Per i vincoli, posizionare when sulla prima riga e mantenere ogni vincolo su una singola riga indipendentemente dalla relativa lunghezza. Posizionare > alla fine dell'ultima riga. Rientro dei vincoli di un livello.

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

Se i parametri o i vincoli di tipo vengono suddivisi, ma non sono presenti parametri di funzione normali, posizionare su = una nuova riga indipendentemente da quanto indicato di seguito:

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

Le stesse regole si applicano alle applicazioni per le funzioni:

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

Formattazione dell'ereditarietà

Gli argomenti per il costruttore della classe base vengono visualizzati nell'elenco di argomenti nella inherit clausola . Inserire la inherit clausola su una nuova riga, rientrata di un livello.

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)

Quando il costruttore è lungo o multilinea, inserirli nella riga successiva, rientrati di un livello.
Formattare questo costruttore su più righe in base alle regole delle applicazioni per le funzioni multilinea.

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

Formattazione del costruttore primario

Nelle convenzioni di formattazione predefinite non viene aggiunto alcuno spazio tra il nome del tipo e le parentesi per il costruttore primario.

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

Più costruttori

Quando la inherit clausola fa parte di un record, inserirla nella stessa riga se è breve. E metterlo sulla riga successiva, rientrato da un livello, se è lungo o multilinea.

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 }

Formattazione degli attributi

Gli attributi vengono posizionati sopra un costrutto:

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

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

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

Devono andare dopo qualsiasi documentazione XML:

// ✔️ OK

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

Formattazione degli attributi nei parametri

Gli attributi possono anche essere inseriti nei parametri. In questo caso, posizionare quindi sulla stessa riga del parametro e prima del nome:

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

Formattazione di più attributi

Quando più attributi vengono applicati a un costrutto che non è un parametro, posizionare ogni attributo su una riga separata:

// ✔️ OK

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

Se applicato a un parametro, posizionare gli attributi sulla stessa riga e separarli con un ; separatore.

Riconoscimenti

Queste linee guida sono basate su una guida completa alle convenzioni di formattazione F# di Anh-Dung Phan.