Indications de mise en forme du code F#

Cet article fournit des instructions pour mettre en forme votre code afin que votre code F# soit :

  • Plus lisible
  • Conforme aux conventions appliquées par les outils de mise en forme dans Visual Studio Code et d’autres éditeurs
  • Similaire à d’autres codes en ligne

Consultez également Conventions de codage de codage et Indication de conception de composants, qui couvrent également les conventions d’affectation de noms.

Mise en forme automatique du code

Le formateur de code Fantomas est l’outil standard de la communauté F# pour la mise en forme automatique du code. Les paramètres par défaut correspondent à ce guide de style.

Nous vous recommandons vivement d’utiliser ce formateur de code. Au sein des équipes F#, les spécifications de mise en forme du code doivent être acceptées et codifiées en termes de fichier de paramètres convenu pour le formateur de code archivé dans le référentiel d’équipe.

Règles générales de mise en forme

Par défaut, F# utilise un espace blanc significatif et respecte cet espace blanc. Les instructions suivantes sont destinées à fournir des conseils sur la façon de négocier certains défis que cela impose.

Utiliser des espaces et non des tabulations

Lorsque la mise en retrait est requise, vous devez utiliser des espaces, et non des tabulations. Le code F# n’utilise pas de tabulations et le compilateur génère une erreur si un caractère de tabulation se trouve en dehors d’un littéral de chaîne ou d’un commentaire.

Utiliser une mise en retrait cohérente

Lors de la mise en retrait, au moins un espace est nécessaire. Votre organisation peut créer des normes de codage pour spécifier le nombre d’espaces à utiliser pour la mise en retrait ; il est courant d’utiliser deux, trois ou quatre espaces de mise en retrait à chaque niveau où la mise en retrait se produit.

Nous vous recommandons quatre espaces par mise en retrait.

Cela dit, la mise en retrait des programmes est une question subjective. Des variantes sont possibles, mais la première règle à suivre est la cohérence de la mise en retrait. Choisissez un style de mise en retrait généralement accepté et utilisez-le systématiquement dans votre codebase.

Éviter la mise en forme sensible à la longueur du nom

Essayez d’éviter la mise en retrait et l’alignement sensibles à l’attribution des noms :

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

Les principales raisons de l’éviter sont les suivantes :

  • Le code important est déplacé à droite
  • La largeur à gauche est moindre pour le code réel
  • Le changement de nom peut interrompre l’alignement

Éviter les espaces blancs superflus

Évitez les espaces blancs superflus dans le code F#, sauf s’ils sont décrits dans ce guide de style.

// ✔️ OK
spam (ham 1)

// ❌ Not OK
spam ( ham 1 )

Mise en forme des commentaires

Préférez plusieurs commentaires à double barre oblique aux commentaires de bloc.

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

Les commentaires doivent mettre en majuscule la première lettre et correspondre à des phrases bien formées.

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

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

Pour la mise en forme des commentaires de documentation XML, consultez « Mise en forme des déclarations » ci-dessous.

Mise en forme des expressions

Cette section décrit la mise en forme des expressions de différents types.

Mise en forme des expressions de chaîne

Les littéraux de chaîne et les chaînes interpolées peuvent simplement être laissés sur une seule ligne, quelle que soit la longueur de la ligne.

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

Les expressions interpolées à plusieurs lignes sont déconseillées. Liez plutôt le résultat de l’expression à une valeur et utilisez-le dans la chaîne interpolée.

Mise en forme des expressions de tuple

Une instanciation de tuple doit se présenter entre parenthèses, et les virgules de limitation à l’intérieur de celui-ci doivent être suivies d’un espace unique, par exemple : (1, 2), (x, y, z).

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

Il est communément admis d'omettre les parenthèses dans les critères spéciaux des tuples :

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

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

Il est également communément admis d’omettre les parenthèses si le tuple correspond à la valeur renvoyée d’une fonction :

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

En résumé, préférez les instanciations de tuples entre parenthèses, mais lorsque vous utilisez des tuples pour des critères spéciaux ou une valeur renvoyée, il est communément admis d’éviter les parenthèses.

Mise en forme des expressions d’application

Lors de la mise en forme d’une fonction ou d’une application de méthode, les arguments sont fournis sur la même ligne si la largeur de ligne autorise ce qui suit :

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

Omettez les parenthèses, sauf si les arguments les exigent :

// ✔️ OK
someFunction1 x.IngredientName

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

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

N’omettez pas les espaces lors de l’appel avec plusieurs arguments curryfiés :

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

Dans les conventions de mise en forme par défaut, un espace est ajouté lors de l’application de fonctions minuscules à des arguments tuplés ou entre parenthèses (même lorsqu’un seul argument est utilisé) :

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

Dans les conventions de mise en forme par défaut, aucun espace n’est ajouté lors de l’application de méthodes majuscules à des arguments tuplés. Cela est dû au fait qu’ils sont souvent utilisés avec la programmation 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)

Vous devrez peut-être passer des arguments à une fonction sur une nouvelle ligne pour plus de lisibilité ou si la liste des arguments ou les noms d’arguments sont trop longs. Dans ce cas, mettez en retrait un niveau :

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

Lorsque la fonction utilise un seul argument tuplé à plusieurs lignes, placez chaque argument sur une nouvelle ligne :

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

Si les expressions d’argument sont courtes, séparez les arguments avec des espaces et conservez-les sur une seule ligne.

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

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

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

Si les expressions d’argument sont longues, utilisez de nouvelles lignes et une mise en retrait à un niveau plutôt qu’une mise en retrait vers la parenthèse gauche.

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

Ces mêmes règles s’appliquent en présence d’un seul argument multiligne, y compris de chaînes multilignes :

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

Mise en forme des expressions de pipeline

Lors de l’utilisation de plusieurs lignes, les opérateurs de pipeline |> doivent se trouver sous les expressions sur lesquelles ils opèrent.

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

Mise en forme d’expressions lambda

Lorsqu’une expression lambda est utilisée en tant qu’argument dans une expression multiligne et suivie d’autres arguments, placez le corps d’une expression lambda sur une nouvelle ligne, avec mise en retrait d’un niveau :

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

Si l’argument lambda correspond au dernier argument d’une application de fonction, placez tous les arguments jusqu’à la flèche sur la même ligne.

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

Traitez les correspondances lambda de la même manière.

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

En présence de nombreux arguments de début ou multilignes avant l’expression lambda, mettez en retrait tous les arguments d’un niveau.

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

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

Si le corps d’une expression lambda compte plusieurs lignes, envisagez de la refactoriser dans une fonction délimitée localement.

Lorsque les pipelines incluent des expressions lambda, chaque expression lambda correspond généralement au dernier argument de chaque étape du 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}")

Si les arguments d’une expression lambda ne tiennent pas sur une ligne ou sont eux-mêmes multilignes, placez-les sur la ligne suivante, avec mise en retrait d’un niveau.

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

Mise en forme des expressions arithmétiques et binaires

Utilisez toujours un espace blanc autour des expressions arithmétiques binaires :

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

Ne pas entourer un opérateur binaire - lorsqu’il est combiné à certains choix de mise en forme, peut conduire à l’interpréter en tant qu’opérateur unaire -. Les opérateurs unaires - doivent toujours être immédiatement suivis de la valeur qu’ils annulent :

// ✔️ OK
let negate x = -x

// ❌ Not OK
let negateBad x = - x

L’ajout d’un caractère d’espace blanc après l’opérateur - peut être source de confusion.

Séparez les opérateurs binaires par espaces. Les expressions infixes sont possibles à des fins d’alignement sur une même colonne :

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

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

Cette règle s’applique également aux unités de mesures dans les types et les annotations constantes :

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

Les opérateurs suivants sont définis dans la bibliothèque standard F# et doivent être utilisés plutôt que de définir des équivalents. L’utilisation de ces opérateurs est recommandée, car elle tend à rendre le code plus lisible et idiomatique. La liste suivante récapitule les opérateurs F# recommandés.

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

Mise en forme des expressions d’opérateur de plage

Ajoutez uniquement des espaces autour de .. lorsque toutes les expressions ne sont pas atomiques. Les entiers et les identificateurs de mot unique sont considérés comme atomiques.

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

Ces règles s’appliquent également au découpage :

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

Mise en forme des expressions if

La mise en retrait des conditions dépend de la taille et de la complexité des expressions qui les composent. Écrivez-les sur une ligne si :

  • cond, e1 et e2 sont courts.
  • e1 et e2 ne sont pas elles-mêmes des expressions if/then/else.
// ✔️ OK
if cond then e1 else e2

Si l’autre expression est absente, il est recommandé de ne jamais écrire l’expression entière sur une seule ligne. Il s’agit de différencier le code impératif de l’élément fonctionnel.

// ✔️ OK
if a then
    ()

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

Si l’une des expressions est multiligne, chaque branche conditionnelle doit être multiligne.

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

Plusieurs conditions avec elif et else sont mises en retrait dans la même étendue que if lorsqu’elles suivent les règles des expressions if/then/else de ligne.

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

Si l’une des conditions ou expressions est multiligne, l’expression entière if/then/else est multiligne :

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

Si une condition est multiligne ou dépasse la tolérance par défaut de la ligne unique, l’expression de condition doit utiliser une mise en retrait et une nouvelle ligne. if et le mot clé then doivent s’aligner lors de l’encapsulation de l’expression de condition longue.

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

Toutefois, il est préférable de refactoriser les conditions longues pour une liaison let ou une fonction distincte :

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

if performAction then
    e1
else
    e2

Mise en forme des expressions de cas d’union

L’application de cas d’union discriminatoire suit les mêmes règles que les applications de fonction et de méthode. Autrement dit, le nom étant mis en majuscules, les formateurs de code suppriment un espace avant un tuple :

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

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

Comme les applications de fonction, les constructions qui se répartissent sur plusieurs lignes doivent utiliser la mise en retrait :

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

Mise en forme des expressions de liste et de tableau

Écrivez x :: l avec des espaces autour de l’opérateur :: (:: est un opérateur infixe, par conséquent entouré d’espaces).

Les listes et tableaux déclarés sur une seule ligne doivent présenter un espace après le crochet ouvrant et avant le crochet fermant :

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

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

Utilisez toujours au moins un espace entre deux opérateurs de type accolades distincts. Par exemple, laissez un espace entre un [ et un {.

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

La même directive s’applique aux listes ou tableaux de tuples.

Les listes et tableaux qui se répartissent sur plusieurs lignes suivent une règle similaire à celle des enregistrements :

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

Comme pour les enregistrements, la déclaration des crochets ouvrants et fermants sur leur propre ligne facilite le déplacement du code et son insertion dans les fonctions :

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

Si une expression de liste ou de tableau correspond au côté droit d’une liaison, vous pouvez préférer l’utilisation du style Stroustrup :

// ✔️ OK
let pascalsTriangle = [| 
   [| 1 |]
   [| 1; 1 |]
   [| 1; 2; 1 |]
   [| 1; 3; 3; 1 |]
   [| 1; 4; 6; 4; 1 |]
   [| 1; 5; 10; 10; 5; 1 |]
   [| 1; 6; 15; 20; 15; 6; 1 |]
   [| 1; 7; 21; 35; 35; 21; 7; 1 |]
   [| 1; 8; 28; 56; 70; 56; 28; 8; 1 |] 
|]

Toutefois, lorsqu’une expression de liste ou de tableau ne correspond pas au côté droit d’une liaison, par exemple lorsqu’elle se trouve à l’intérieur d’une liste ou d’un tableau autre, si cette expression interne doit s’étendre sur plusieurs lignes, les crochets doivent se trouver sur leurs propres lignes :

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

Cette même règle s’applique aux types d’enregistrements à l’intérieur des tableaux/listes :

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

Lors de la génération de tableaux et de listes par programmation, préférez -> plutôt que do ... yield lorsqu’une valeur est toujours générée :

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

Les versions antérieures de F# nécessitaient la spécification de yield dans les situations où les données pouvaient être générées conditionnellement ou en présence d’expressions consécutives à évaluer. Il est préférable d’omettre ces mots clés yield, sauf si vous devez compiler avec une version antérieure du langage 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"
    ]

Dans certains cas, do...yield peut faciliter la lisibilité. Ces cas, bien que subjectifs, doivent être pris en considération.

Mise en forme des expressions d’enregistrement

Les enregistrements courts peuvent être écrits sur une seule ligne :

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

Les enregistrements plus longs doivent utiliser de nouvelles lignes pour les étiquettes :

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

Styles de mise en forme entre crochets multilignes

Pour les enregistrements qui s’étendent sur plusieurs lignes, il existe trois styles de mise en forme couramment utilisés : Cramped, Aligned et Stroustrup. Le style Cramped est le style par défaut pour le code F#, car il tend à privilégier les styles qui permettent au compilateur d’analyser facilement le code. Les styles Aligned et Stroustrup permettent de réorganiser plus facilement les membres, ce qui peut être plus facile à refactoriser, mais certaines situations peuvent nécessiter un code légèrement plus détaillé.

  • Cramped : format d’enregistrement F# par défaut et standard historique. Les crochets ouvrants sont sur la même ligne que le premier membre et le crochet fermant sur la même ligne que le dernier membre.

    let rainbow = 
        { Boss1 = "Jeffrey"
          Boss2 = "Jeffrey"
          Boss3 = "Jeffrey"
          Lackeys = [ "Zippy"; "George"; "Bungle" ] }
    
  • Aligned : les crochets ont chacun leur propre ligne, alignés sur la même colonne.

    let rainbow =
        {
            Boss1 = "Jeffrey"
            Boss2 = "Jeffrey"
            Boss3 = "Jeffrey"
            Lackeys = ["Zippy"; "George"; "Bungle"]
        }
    
  • Stroustrup :le crochet ouvrant se trouve sur la même ligne que la liaison et le crochet fermant a sa propre ligne.

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

Les mêmes règles de style de mise en forme s’appliquent aux éléments de liste et de tableau.

Mise en forme des expressions d’enregistrement de copie et de mise à jour

Une expression d’enregistrement de copie et de mise à jour est toujours un enregistrement et dès lors, des instructions similaires s’appliquent.

Les expressions courtes peuvent tenir sur une seule ligne :

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

Les expressions plus longues doivent utiliser de nouvelles lignes et être mises en forme en fonction de l’une des conventions nommées ci-dessus :

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

Remarque : si vous utilisez le style Stroustrup pour les expressions de copie et de mise à jour, vous devez mettre en retrait les membres au-delà nom de l’enregistrement copié :

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

Mise en forme des critères spéciaux

Utilisez un | pour chaque clause d’une correspondance sans retrait. En présence d’une expression courte, vous pouvez envisager d’utiliser une seule ligne si chaque sous-expression est également simple.

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

Si l’expression à droite de la flèche de critères spéciaux est trop longue, déplacez-la vers la ligne suivante, avec mise en retrait d’une étape à partir de match/|.

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

Comme pour les conditions if importantes, si une expression de correspondance est multiligne ou dépasse la tolérance par défaut de ligne unique, l’expression de correspondance doit utiliser une mise en retrait et une nouvelle ligne. match et le mot clé with doivent s’aligner lors de l’encapsulation de l’expression de correspondance longue.

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

Toutefois, il est préférable de refactoriser les expressions de correspondance longues pour une liaison let ou une fonction distincte :

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

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

L’alignement des flèches de critères spéciaux doit être évité.

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

Les critères spéciaux introduits à l’aide du mot clé function doivent présenter une mise en retrait d’un niveau à partir du début de la ligne précédente :

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

L’utilisation de function dans les fonctions définies par let ou let rec doit généralement être évitée pour privilégier match. Si elles sont utilisées, les règles de modèle doivent s’aligner sur le mot clé 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

Mise en forme des expressions try/with

Les critères spéciaux du type d’exception doivent être mis en retrait au même niveau que 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"

Ajoutez un | pour chaque clause, sauf s’il n’existe qu’une seule clause :

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

Mise en forme des arguments nommés

Les arguments nommés doivent avoir des espaces entourant = :

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

Lors de l’utilisation de critères spéciaux à l’aide d’unions discriminatoires, les modèles nommés sont mis en forme de la même façon, par exemple.

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

Mise en forme des expressions de mutation

Généralement, les expressions de mutation location <- expr sont mises en forme sur une ligne. Si une mise en forme multiligne est requise, placez l’expression de droite sur une nouvelle ligne.

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

Mise en forme des expressions d’objet

Les membres des expressions d’objet doivent être alignés sur memberavec mise en retrait d’un niveau.

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

Vous pouvez également préférer utiliser le style Stroustrup :

let comparer = { 
    new IComparer<string> with
        member x.Compare(s1, s2) =
            let rev (s: String) = new String(Array.rev (s.ToCharArray()))
            let reversed = rev s1
            reversed.CompareTo(rev s2)
}

Les définitions de type vides peuvent être mises en forme sur une ligne :

type AnEmptyType = class end

Quelle que soit la largeur de page choisie, = class end doit toujours se trouver sur la même ligne.

Mise en forme des expressions d’index/de section

Les expressions d’index ne doivent contenir aucun espace autour des crochets ouvrants et fermants.

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

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

Cela s’applique également à l’ancienne syntaxe expr.[idx].

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

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

Mise en forme des expressions entre guillemets

Les symboles délimiteurs (<@, @>, <@@, @@>) doivent être placés sur des lignes distinctes si l’expression entre guillemets est une expression multiligne.

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

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

Dans les expressions à une seule ligne, les symboles délimiteurs doivent être placés sur la même ligne que l’expression elle-même.

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

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

Mise en forme des expressions chaînées

Lorsque les expressions chaînées (applications de fonction entrelacées avec .) sont longues, placez chaque appel d’application sur la ligne suivante. Mettez en retrait les liaisons suivantes de la chaîne d’un niveau après la liaison de début.

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

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

La liaison de début peut être composée de plusieurs liaisons s’il s’agit d’identificateurs simples. Par exemple, l’ajout d’un espace de noms complet.

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

Les liaisons suivantes doivent également contenir des identificateurs simples.

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

Si les arguments d’une application de fonction ne tiennent pas sur le reste de la ligne, placez chaque argument sur la ligne suivante.

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

Les arguments lambda d’une application de fonction doivent commencer sur la même ligne que le ( ouvrant.

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

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

Mise en forme des déclarations

Cette section décrit la mise en forme des déclarations de différents types.

Ajouter des lignes vides entre les déclarations

Séparez les définitions de fonction et de classe de niveau supérieur avec une seule ligne vide. Par exemple :

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

Si une construction comporte des commentaires de document XML, ajoutez une ligne vide avant le commentaire.

// ✔️ OK

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

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

Mise en forme des déclarations let et membres

Lors de la mise en forme de déclarations let et member, le côté droit d’une liaison passe généralement sur une ligne, ou (s’il est trop long) sur une nouvelle ligne mise en retrait d’un niveau.

Les exemples suivants sont conformes :

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

Ceux-ci ne sont pas conformes :

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

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

Les instanciations de type d’enregistrement peuvent également placer les crochets sur leurs propres lignes :

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

Vous pouvez également préférer utiliser le style Stroustrup, avec le { ouvrant sur la même ligne que le nom de liaison :

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

Séparez les membres avec une seule ligne vide et un document et ajoutez un commentaire de documentation :

// ✔️ OK

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

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

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

Des lignes vides supplémentaires peuvent être utilisées (avec parcimonie) pour séparer les groupes de fonctions associées. Les lignes vides peuvent être omises entre plusieurs unilignes associées (par exemple, un ensemble d’implémentations factices). Utilisez des lignes vides dans les fonctions, avec parcimonie, pour indiquer des sections logiques.

Mise en forme des arguments de fonction et de membre

Lors de la définition d’une fonction, utilisez un espace blanc autour de chaque argument.

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

En présence d’une définition de fonction longue, placez les paramètres sur de nouvelles lignes et mettez-les en retrait pour qu’ils correspondent au niveau de retrait du paramètre suivant.

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

Cela s’applique également aux membres, constructeurs et paramètres utilisant des tuples :

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

Si les paramètres sont curryfiés, placez le caractère = avec n’importe quel type de retour sur une nouvelle ligne :

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

Cela permet d’éviter les lignes trop longues (dans le cas où le type de retour peut avoir un nom long) et de limiter les dommages de ligne lors de l’ajout de paramètres.

Mise en forme des déclarations d’opérateur

Vous pouvez utiliser un espace blanc pour entourer une définition d’opérateur :

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

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

Pour tout opérateur personnalisé commençant par * et doté de plusieurs caractères, vous devez ajouter un espace blanc au début de la définition afin d’éviter une ambiguité du compilateur. Dès lors, nous vous recommandons simplement d’entourer les définitions de tous les opérateurs avec un seul caractère d’espace blanc.

Mise en forme des déclarations d’enregistrement

Pour les déclarations d’enregistrement, par défaut, vous devez mettre en retrait le { dans la définition de type par quatre espaces, démarrer la liste des étiquettes sur la même ligne et aligner les membres, le cas échéant, avec le jeton { :

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

Il est également courant de placer des crochets sur leur propre ligne, avec des étiquettes mises en retrait par quatre espaces supplémentaires :

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

Vous pouvez également placer le { à la fin de la première ligne de définition de type (style Stroustrup) :

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

Si des membres supplémentaires sont nécessaires, évitez with/end chaque fois que possible :

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

Il existe une exception à cette règle de style si vous mettez en forme des enregistrements en fonction du style Stroustrup. Dans ce cas, en raison de règles de compilateur, le mot clé with est requis si vous souhaitez implémenter une interface ou ajouter des membres supplémentaires :

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

Lorsqu’une documentation XML est ajoutée pour les champs d’enregistrement, le style Aligned ou Stroustrup est privilégié et un espace blanc supplémentaire doit être ajouté entre les membres :

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

Il est préférable de placer le jeton d’ouverture et le jeton de fermeture sur une nouvelle ligne si vous déclarez des implémentations d’interface ou des membres sur l’enregistrement :

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

Ces mêmes règles s’appliquent aux alias de type d’enregistrement anonyme.

Mise en forme des déclarations d’union discriminée

Pour les déclarations d’union discriminées, mettez en retrait | dans la définition de type par quatre espaces :

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

En présence d’une seule union courte, vous pouvez omettre le premier |.

// ✔️ OK
type Address = Address of string

Pour une union plus longue ou multiligne, conservez le | et placez chaque champ d’union sur une nouvelle ligne, avec le * de séparation à la fin de chaque ligne.

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

Lorsque des commentaires de documentation sont ajoutés, utilisez une ligne vide avant chaque commentaire ///.

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

Mise en forme des déclarations de littéraux

Les littéraux F# utilisant l’attribut Literal doivent placer l’attribut sur sa propre ligne et utiliser la casse Pascal :

// ✔️ OK

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

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

Évitez de placer l’attribut sur la même ligne que la valeur.

Mise en forme des déclarations de module

Le code d’un module local doit être mis en retrait par rapport au module, mais le code d’un module de niveau supérieur ne doit pas l’être. Il n’est pas nécessaire de mettre en retrait les éléments d’espace de noms.

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

Mise en forme des déclarations do

Dans les déclarations de type, les déclarations de module et les expressions de calcul, l’utilisation de do ou do! est parfois nécessaire pour les opérations à effet secondaire. Lorsqu’elles s’étendent sur plusieurs lignes, utilisez la mise en retrait et une nouvelle ligne pour une mise en retrait cohérente avec let/let!. Voici un exemple d’utilisation de do dans une 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

Voici un exemple avec do! utilisant deux espaces de mise en retrait (car avec do! il n’existe aucune différence entre les approches lors de l’utilisation de quatre espaces de mise en retrait) :

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

Mise en forme des opérations d’expression de calcul

Lors de la création d’opérations personnalisées pour les expressions de calcul, il est recommandé d’utiliser la casse mixte :

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

Le domaine qui est modélisé doit déterminer la convention d’affectation de noms. S’il est idiomatique d’utiliser une autre convention, cette convention doit être utilisée.

Si la valeur renvoyée d’une expression est une expression de calcul, il est préférable de placer le nom du mot clé d’expression de calcul sur sa propre ligne :

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

Vous pouvez également préférer placer l’expression de calcul sur la même ligne que le nom de liaison :

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

Quelle que soit votre préférence, vous devez vous efforcer de rester cohérent dans votre codebase. Les formateurs peuvent vous permettre de spécifier cette préférence à des fins de cohérence.

Mise en forme des types et annotations de type

Cette section décrit la mise en forme des types et annotations de type. Cela inclut la mise en forme des fichiers de signature avec l’extension .fsi.

Pour les types, préférez la syntaxe de préfixe pour les génériques (Foo<T>), avec certaines exceptions spécifiques

F# permet le style suffixé d’écriture de types génériques (par exemple, int list) et le style préfixé (par exemple, list<int>). Le style suffixé ne peut être utilisé qu’avec un seul argument de type. Privilégiez toujours le style .NET, sauf pour cinq types spécifiques :

  1. Pour les listes F#, utilisez le formulaire suffixé : int list plutôt que list<int>.
  2. Pour les options F#, utilisez le formulaire suffixé : int option plutôt que option<int>.
  3. Pour les options de valeur F#, utilisez le formulaire suffixé : int voption plutôt que voption<int>.
  4. Pour les tableaux F#, utilisez le formulaire suffixé : int array plutôt que array<int> ou int[].
  5. Pour les cellules de référence, utilisez int ref plutôt que ref<int> ou Ref<int>.

Pour tous les autres types, utilisez le formulaire préfixé.

Mise en forme des types de fonctions

Lorsque vous définissez la signature d’une fonction, utilisez un espace blanc autour du symbole -> :

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

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

Mise en forme des annotations de type valeur et argument

Lorsque vous définissez des valeurs ou des arguments avec des annotations de type, utilisez un espace blanc après le symbole :, mais pas avant :

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

Mise en forme des annotations de type multiligne

Lorsqu’une annotation de type est longue ou multiligne, placez-la sur la ligne suivante, mise en retrait d’un niveau.

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

Pour les types d’enregistrements anonymes inlined, vous pouvez également utiliser le style Stroustrup :

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

Mise en forme des annotations de type de retour

Dans les annotations de type de retour de fonction ou de membre, utilisez un espace blanc avant et après le symbole : :

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

Types de mise en forme dans les signatures

Lorsque vous écrivez des types de fonctions complets dans des signatures, il est parfois nécessaire de fractionner les arguments sur plusieurs lignes. Le type de retour est toujours mis en retrait.

Pour une fonction tuplée, les arguments sont séparés par *, placés à la fin de chaque ligne.

Par exemple, considérez une fonction avec l’implémentation suivante :

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

Dans le fichier de signature (extension .fsi) correspondant, la fonction peut être mise en forme comme suit lorsqu’une mise en forme multiligne est requise :

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

De même, envisagez une fonction curryfiée :

let SampleCurriedFunction arg1 arg2 arg3 arg4 = ...

Dans le fichier de signature correspondant, les éléments -> sont placés à la fin de chaque ligne :

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

De même, envisagez une fonction combinant des arguments curryfiés et tuplés :

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

Dans le fichier de signature correspondant, les types précédés d’un tuple sont mis en retrait.

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

Les mêmes règles s’appliquent aux membres des signatures de type :

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

Mise en forme d’arguments et de contraintes de type générique explicites

Les instructions ci-dessous s’appliquent aux définitions de fonction, de membre et de type, ainsi qu’aux applications de fonction.

Conservez les arguments et contraintes de type générique sur une seule ligne s’ils ne sont pas trop longs :

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

Si les arguments/contraintes de type générique et les paramètres de fonction ne conviennent pas, mais que les paramètres/contraintes de type seuls conviennent, placez les paramètres sur de nouvelles lignes :

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

Si les paramètres ou les contraintes de type sont trop longs, scindez-les et alignez-les comme indiqué ci-dessous. Conservez la liste des paramètres de type sur la même ligne que la fonction, quelle que soit sa longueur. Pour les contraintes, placez when sur la première ligne et conservez chaque contrainte sur une seule ligne, quelle que soit sa longueur. Placez > à la fin de la dernière ligne. Mettre en retrait les contraintes d’un niveau.

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

Si les paramètres/contraintes de type sont scindés, mais qu’il n’existe pas de paramètres de fonction normaux, placez = sur une nouvelle ligne :

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

Les mêmes règles s’appliquent aux applications de fonction :

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

Mise en forme de l’héritage

Les arguments du constructeur de classe de base apparaissent dans la liste d’arguments de la clause inherit. Placez la clause inherit sur une nouvelle ligne, mise en retrait d’un niveau.

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)

En présence d’un constructeur long ou multiligne, placez-le sur la ligne suivante, avec mise en retrait d’un niveau.
Mettez en forme ce constructeur multiligne en fonction des règles des applications de fonction multiligne.

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

Mise en forme du constructeur principal

Dans les conventions de mise en forme par défaut, aucun espace n’est ajouté entre le nom de type et les parenthèses du constructeur principal.

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

Plusieurs constructeurs

Lorsque la clause inherit fait partie d’un enregistrement, placez-la sur la même ligne si elle est courte. Placez-la sur la ligne suivante, avec mise en retrait d’un niveau, si elle est longue ou multiligne.

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 }

Mise en forme des attributs

Les attributs sont placés au-dessus d’une construction :

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

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

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

Ils doivent suivre n’importe quelle documentation XML :

// ✔️ OK

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

Mise en forme des attributs sur des paramètres

Les attributs peuvent également être placés sur des paramètres. Dans ce cas, placez-les sur la même ligne que le paramètre et avant le nom :

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

Mise en forme de plusieurs attributs

Lorsque plusieurs attributs sont appliqués à une construction autre qu’un paramètre, placez chaque attribut sur une ligne distincte :

// ✔️ OK

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

Lorsqu’ils sont appliqués à un paramètre, placez les attributs sur la même ligne et séparez-les par un séparateur ;.

Remerciements

Ces instructions sont basées sur le Guide complet des conventions de mise en forme F# par Anh-Dung Phan.