Instrucciones de formato de código de F#

En este artículo se ofrecen instrucciones de cómo aplicar formato al código para que el código de F#:

  • Sea más legible
  • Vaya de acuerdo con las convenciones aplicadas por las herramientas de formato de Visual Studio Code y otros editores
  • Sea similar a otro código en línea

Consulte también Convenciones de código y Directrices de diseño de componentes, que también abarca las convenciones de nomenclatura.

Aplicación automática de formato al código

El formateador de código Fantomas es la herramienta estándar de la comunidad de F# para la aplicación automática de formato al código. La configuración predeterminada corresponde a esta guía de estilo.

Se recomienda encarecidamente el uso de este formateador de código. En los equipos de F#, las especificaciones de formato de código deben acordarse y codificarse en un archivo de configuración acordado para el formateador de código insertado en el repositorio del equipo.

Reglas generales para el formato

F# usa numerosos espacios en blanco de forma predeterminada y es sensible a estos. Las instrucciones siguientes están pensadas para proporcionar una guía sobre cómo sortear algunos obstáculos que esta característica puede imponer.

Uso de espacios, no tabulaciones

Cuando se requiera sangría, debe usar espacios, no tabulaciones. El código de F# no usa tabulaciones y el compilador producirá un error si se encuentra un carácter de tabulación fuera de un literal o comentario de cadena.

Uso de sangría de forma coherente

Al aplicar sangría, se requiere al menos un espacio. Su organización puede crear estándares de creación de código para especificar el número de espacios que se van a usar para la sangría; lo habitual son dos, tres o cuatro espacios de sangría en cada nivel donde se produce la sangría.

Se recomiendan cuatro espacios por sangría.

Dicho esto, la sangría de los programas es una cuestión subjetiva. Se aceptan variaciones, pero la primera regla que debe seguir es la coherencia de la sangría. Elija un estilo de sangría aceptado generalmente y úselo sistemáticamente en todo el código base.

Evite formato que sea sensible a la longitud de los nombres.

Intente evitar sangría y alineación que sean sensibles a la nomenclatura:

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

Las principales razones para evitarlo son:

  • El código importante se desplaza a la derecha.
  • Queda menos ancho para el código real.
  • Cambiar el nombre puede romper la alineación.

Evitar espacios en blanco extraños

Evite espacios en blanco extraños en el código de F#, excepto donde se describe en esta guía de estilo.

// ✔️ OK
spam (ham 1)

// ❌ Not OK
spam ( ham 1 )

Aplicación de formato a los comentarios

Se prefieren varios comentarios de barra diagonal doble sobre los comentarios de bloque.

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

En los comentarios debe ir en mayúscula la primera letra y deben ser frases u oraciones bien formadas.

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

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

Para información sobre el formato de los comentarios de documentación XML, consulte "Declaraciones de formato" a continuación.

Aplicación de formato a expresiones

En esta sección se describen la aplicación de formato a expresiones de diferentes tipos.

Aplicación de formato a expresiones de cadena

Los literales de cadena y las cadenas interpoladas solo se pueden dejar en una línea, con independencia de lo larga que sea la línea.

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

No se recomienda usar expresiones interpoladas de varias líneas. En su lugar, enlace el resultado de la expresión a un valor y úselo en la cadena interpolada.

Aplicación de formato a expresiones de tupla

Una creación de instancias de tupla debe estar entre paréntesis y las comas delimitadoras dentro de la instancia deben ir seguidas de un solo espacio, por ejemplo: (1, 2), (x, y, z).

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

Normalmente se acepta omitir paréntesis en la coincidencia de patrones de tuplas:

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

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

También se acepta normalmente omitir paréntesis si la tupla es el valor devuelto de una función:

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

En resumen, se prefieren las instancias de tupla entre paréntesis, pero cuando se usan tuplas para la coincidencia de patrones o un valor devuelto, se considera correcto evitar los paréntesis.

Aplicación de formato a expresiones de aplicación

Al dar formato a una aplicación de método o función, los argumentos se proporcionan en la misma línea cuando el ancho de línea lo permite:

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

Omita paréntesis a menos que los argumentos los requieran:

// ✔️ OK
someFunction1 x.IngredientName

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

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

No omita espacios al invocar con varios argumentos currificados:

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

En las convenciones de formato predeterminadas, se agrega un espacio al aplicar funciones en minúscula a argumentos entre paréntesis o en tupla (incluso cuando se usa un único argumento):

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

En las convenciones de formato predeterminadas, no se agrega ningún espacio al aplicar métodos en mayúsculas a argumentos en tupla. Esto se debe a que a menudo se usan con programación fluida:

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

Es posible que tenga que pasar argumentos a una función en una nueva línea por motivos de legibilidad o porque la lista de argumentos o los nombres de argumento son demasiado largos. En ese caso, aplique sangría de un nivel:

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

Cuando la función tome un único argumento en tupla de varias líneas, coloque cada argumento en una nueva línea:

// ✔️ 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 las expresiones de argumento son cortas, separe los argumentos con espacios y manténgalos en una sola línea.

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

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

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

Si las expresiones de argumento son largas, use nuevas líneas y sangría de un nivel, en lugar de aplicar sangría a la paréntesis izquierdos.

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

Las mismas reglas se aplican aunque solo haya un único argumento de varias líneas, incluidas las cadenas de varias líneas:

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

Aplicación de formato a expresiones de canalización

Al usar varias líneas, los operadores de canalización |> deben ir debajo de las expresiones en las que operan.

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

Aplicación de formato a expresiones lambda

Cuando una expresión lambda se usa como argumento en una expresión de varias líneas y va seguida de otros argumentos, coloque el cuerpo de una expresión lambda en una nueva línea, con sangría de un nivel:

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

Si el argumento lambda es el último argumento de una aplicación de función, coloque todos los argumentos hasta la flecha en la misma línea.

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

Trate la expresión lambda de coincidencia de una manera similar.

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

Cuando haya muchos argumentos iniciales o de varias líneas antes de la expresión lambda, aplique sangría de un nivel a todos los argumentos.

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

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

Si el cuerpo de una expresión lambda es de varias líneas de longitud, debe considerar la posibilidad de refactorizarlo en una función con ámbito local.

Cuando las canalizaciones incluyen expresiones lambda, cada expresión lambda suele ser el último argumento en cada fase de la canalización:

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

En caso de que los argumentos de una expresión lambda no se ajusten a una sola línea o sean multilínea, colóquelos en la siguiente línea, aplicando sangría de un nivel.

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

Aplicación de formato a expresiones aritméticas y binarias

Use siempre espacios en blanco alrededor de expresiones aritméticas binarias:

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

No rodear un operador binario -, cuando se combina con determinadas opciones de formato, podría dar lugar a que se interpretara como un operador - unario. Los operadores - unarios siempre deben ir seguidos inmediatamente del valor que niegan:

// ✔️ OK
let negate x = -x

// ❌ Not OK
let negateBad x = - x

Agregar un carácter de espacio en blanco después del operador - puede provocar confusión para otros usuarios.

Separe los operadores binarios con espacios. Las expresiones de infijo son aceptables para la alineación en la misma columna:

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

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

Esta regla también se aplica a unidades de medidas en anotaciones de tipos y 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)> }

Los operadores siguientes se definen en la biblioteca estándar de F# y se deben usar en lugar de definir equivalentes. Se recomienda usar estos operadores, ya que tienden a hacer que el código sea más legible e idiomático. En la lista siguiente se resumen los operadores de F# recomendados.

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

Aplicación de formato a expresiones de operador de intervalo

Agregue solo espacios alrededor de .. cuando todas las expresiones no sean atómicas. Los enteros y los identificadores de palabra única se consideran atómicos.

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

Estas reglas también se aplican a la segmentación:

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

Aplicación de formato a expresiones condicionales (if)

La sangría de las condicionales depende del tamaño y la complejidad de las expresiones que las componen. Escríbalas en una línea cuando:

  • cond, e1, y e2 son cortos.
  • e1 y e2 no sean expresiones if/then/else por sí mismas.
// ✔️ OK
if cond then e1 else e2

Si la expresión else no está presente, se recomienda no escribir nunca toda la expresión en una línea. El motivo es diferenciar el código imperativo del funcional.

// ✔️ OK
if a then
    ()

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

Si alguna de las expresiones es de varias líneas, cada rama condicional debe ser de varias líneas.

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

Se aplica sangría a varios condicionales con elif y else en el mismo ámbito que if cuando siguen las reglas de las expresiones if/then/else de una línea.

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

Si alguna de las condiciones o expresiones es de varias líneas, toda la expresión if/then/else es de varias líneas:

// ✔️ 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 una condición es de varias líneas o supera la tolerancia predeterminada de la línea única, la expresión de condición debe usar una sangría y una nueva línea. La palabra clave if y then debe alinearse al encapsular la expresión de condición larga.

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

Sin embargo, es mejor el estilo de refactorizar condiciones largas en un enlace let o una función independiente:

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

if performAction then
    e1
else
    e2

Aplicación de formato a expresiones de mayúsculas y minúsculas de unión

La aplicación de mayúsculas y minúsculas de unión discriminadas sigue las mismas reglas que las aplicaciones de método y función. Es decir, dado que el nombre está en mayúsculas, los formateadores de código quitarán un espacio delante de una tupla:

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

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

Al igual que las aplicaciones de función, las construcciones que se dividen entre varias líneas deben usar sangría:

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

Aplicación de formato a expresiones de lista y matriz

Escriba x :: l con espacios alrededor del operador :: (:: es un operador infijo, de ahí que esté rodeado por espacios).

En la lista y las matrices declaradas en una sola línea debe haber un espacio después del corchete de apertura y antes del corchete de cierre:

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

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

Use siempre al menos un espacio entre dos operadores distintos de tipo llave. Por ejemplo, deje un espacio entre [ y {.

// ✔️ 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 misma directriz se aplica a listas o matrices de tuplas.

Las listas y matrices que se dividen entre varias líneas siguen una regla similar a la de los registros:

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

Al igual que sucede con los registros, declarar los corchetes de apertura y cierre en su propia línea hará que sea más fácil desplazar el código e introducirlo en las funciones:

// ✔️ 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 una expresión de lista o matriz es el lado derecho de un enlace, es posible que prefiera usar el estilo 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 |] 
|]

Sin embargo, cuando una expresión de lista o matriz no es el lado derecho de un enlace, como cuando está dentro de otra lista o matriz, si esa expresión interna necesita abarcar varias líneas, los corchetes deben ir en sus propias líneas:

// ✔️ 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 misma regla se aplica a los tipos de registro dentro de matrices o listas:

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

Al generar matrices y listas mediante programación, se prefiere -> sobre do ... yield cuando siempre se genera un valor:

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

Las versiones anteriores de F# requerían especificar yield en situaciones en las que los datos se pueden generar condicionalmente o pueda haber expresiones consecutivas que se vayan a evaluar. Es preferible omitir estas palabras clave yield a menos que la compilación la realice con una versión anterior del lenguaje 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"
    ]

En algunos casos, do...yield puede contribuir a la legibilidad. Estos casos, aunque subjetivas, deben tenerse en cuenta.

Aplicación de formato a expresiones de registro

Los registros cortos se pueden escribir en una sola línea:

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

Los registros que son más largos deben usar nuevas líneas para las etiquetas:

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

Estilos de formato de corchetes de varias líneas

En el caso de registros que abarcan varias líneas, hay tres estilos de formato usados habitualmente: Cramped, Alignedy Stroustrup. El estilo Cramped ha sido el estilo predeterminado en el código de F#, ya que tiende a favorecer los estilos que permiten al compilador analizar fácilmente el código. Los estilos Aligned y Stroustrup permiten una reordenación más sencilla de los miembros, lo que conduce a que el código pueda ser más fácil de refactorizar, con el inconveniente de que en ciertas situaciones puede que haga falta un código ligeramente más detallado.

  • Cramped: el estándar histórico y el formato de registro de F# predeterminado. Los corchetes de apertura van en la misma línea que el primer miembro y el corchete de cierre en la misma línea que el último miembro.

    let rainbow = 
        { Boss1 = "Jeffrey"
          Boss2 = "Jeffrey"
          Boss3 = "Jeffrey"
          Lackeys = [ "Zippy"; "George"; "Bungle" ] }
    
  • Aligned: cada corchete tiene su propia línea, alineada en la misma columna.

    let rainbow =
        {
            Boss1 = "Jeffrey"
            Boss2 = "Jeffrey"
            Boss3 = "Jeffrey"
            Lackeys = ["Zippy"; "George"; "Bungle"]
        }
    
  • Stroustrup: el corchete de apertura va en la misma línea que el enlace y el de cierre tiene su propia línea.

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

Se aplican las mismas reglas de estilo de formato a los elementos de lista y matriz.

Aplicación de formato a expresiones de registro de copia y actualización

Una expresión de registro de copia y actualización sigue siendo un registro, por lo que se aplican directrices similares.

Las expresiones cortas pueden caber en una línea:

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

Las expresiones más largas deben usarse en nuevas líneas, y el formato debe basarse en una de las convenciones mencionadas anteriormente:

// ✔️ 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: Si usa el estilo Stroustrup para las expresiones de copia y actualización, debe aplicar sangría a los miembros más allá del nombre del registro copiado:

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

Aplicación de formato a la coincidencia de patrones

Use una | para cada cláusula de una coincidencia sin sangría. Si la expresión es corta, puede considerar el uso de una sola línea si cada subexpresión también es 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 la expresión situada a la derecha de la flecha de coincidencia de patrones es demasiado grande, aplíquele sangría un paso desde match/|.

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

De forma similar a las condiciones if largas, si una expresión de coincidencia tiene varias líneas o supera la tolerancia predeterminada de la línea única, la expresión de coincidencia debe usar una sangría y una nueva línea. La palabra clave match y with debe alinearse al encapsular la expresión de coincidencia larga.

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

Sin embargo, es mejor el estilo de refactorizar condiciones de coincidencia largas en un enlace let o una función independiente:

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

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

Debe evitarse alinear las flechas de una coincidencia de patrones.

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

Se debe aplica sangría de un nivel a la coincidencia de patrones introducida mediante la palabra clave function desde el principio de la línea anterior:

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

El uso de function en funciones definidas por let o let rec debe evitarse en general en favor de match. Si se usa, las reglas de patrón deben alinearse con la palabra clave 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

Aplicación de formato a expresiones try/with

Se debe aplica el mismo nivel de sangría a la coincidencia de patrones del tipo de excepción que a 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"

Agregue una | a cada cláusula, excepto cuando solo haya una cláusula:

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

Aplicación de formato a argumentos con nombre

Los argumentos con nombre deben tener espacios que rodean la =:

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

Cuando la coincidencia de patrones usa uniones discriminadas, los patrones con nombre reciben un formato similar, por ejemplo.

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

Aplicación de formato a expresiones de mutación

Normalmente, las expresiones de mutación location <- expr tienen el formato de una línea. Si se requiere un formato de varias líneas, coloque la expresión del lado derecho en una nueva línea.

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

Aplicación de formato a expresiones de objeto

Los miembros de la expresión de objeto deben estar alineados con member, con un nivel de sangría.

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

También puede preferir usar el estilo 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)
}

A las definiciones de tipo vacías se les puede asignar el formato de que estén en una sola línea:

type AnEmptyType = class end

Independientemente del ancho de página elegido, = class end siempre debe estar en la misma línea.

Aplicación de formato a expresiones de índice o segmento

Las expresiones de índice no deben contener espacios alrededor de los corchetes de apertura y cierre.

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

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

Lo mismo se aplica a la sintaxis expr.[idx] anterior.

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

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

Aplicación de formato a expresiones entre comillas

Los símbolos delimitadores (<@ , @>, <@@, @@>) deben colocarse en líneas distintas si la expresión entre comillas es una expresión de varias líneas.

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

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

En las expresiones de una sola línea, los símbolos delimitadores deben colocarse en la misma línea que la propia expresión.

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

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

Aplicación de formato a expresiones encadenadas

Cuando las expresiones encadenadas (las aplicaciones de función entrelazadas con .) sean largas, coloque cada invocación de aplicación en la línea siguiente. Aplique un nivel de sangría a los vínculos posteriores de la cadena después del vínculo inicial.

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

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

El vínculo inicial se puede componer de varios vínculos si son identificadores simples. Por ejemplo, la adición de un espacio de nombres completo.

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

Los vínculos posteriores también deben contener identificadores 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))

Cuando los argumentos dentro de una aplicación de función no quepan en el resto de la línea, coloque cada argumento en la línea siguiente.

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

Los argumentos lambda dentro de una aplicación de función deben iniciarse en la misma línea que el corchete de apertura (().

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

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

Aplicación de formato a declaraciones

En esta sección se describe la aplicación de formato a declaraciones de diferentes tipos.

Adición de líneas en blanco entre declaraciones

Separe las definiciones de clase y función de nivel superior con una sola línea en blanco. Por ejemplo:

// ✔️ 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 una construcción tiene comentarios de documentación XML, agregue una línea en blanco delante.

// ✔️ OK

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

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

Aplicación de formato a declaraciones let y member

Cuando se aplica formato a declaraciones let y member, normalmente el lado derecho de un enlace va en una línea o (si es demasiado largo) pasa a una nueva línea con sangría de un nivel.

Por ejemplo, los siguientes ejemplos son aceptables:

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

Estos no lo son:

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

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

Las instancias de tipo de registro también pueden colocar los corchetes en sus propias líneas:

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

Puede que también prefiera usar el estilo Stroustrup, con la apertura { en la misma línea que el nombre del enlace:

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

Separe los miembros con una sola línea en blanco y documente y agregue un comentario de documentación:

// ✔️ OK

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

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

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

Se pueden usar líneas en blanco adicionales (con moderación) para separar grupos de funciones relacionadas. Las líneas en blanco se pueden omitir entre un montón de comentarios relacionados (por ejemplo, un conjunto de implementaciones ficticias). Use líneas en blanco en funciones, con moderación, para indicar secciones lógicas.

Aplicación de formato a argumentos de miembro y función

Al definir una función, use espacios en blanco alrededor de cada argumento.

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

Si tiene una definición de función larga, coloque los parámetros en nuevas líneas y aplíqueles sangría para que coincidan con el nivel de sangría del parámetro subsiguiente.

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

Esto también se aplica a miembros, constructores y parámetros que usan tuplas:

// ✔️ 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 los parámetros están currificados, coloque el carácter = junto con cualquier tipo de valor devuelto en una nueva línea:

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

Esta es una manera de evitar líneas demasiado largas (en caso de que el tipo de valor devuelto tenga un nombre largo) y tener menos daños en las líneas al agregar parámetros.

Aplicación de formato a declaraciones de operador

Opcionalmente, use espacios en blanco para rodear una definición de operador:

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

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

Para los operadores personalizados que comiencen por * y tengan más de un carácter, debe agregar un espacio en blanco al principio de la definición para evitar la ambigüedad del compilador. Por este motivo, se recomienda simplemente rodear las definiciones de todos los operadores con un solo carácter de espacio en blanco.

Aplicación de formato a declaraciones de registro

Para las declaraciones de registro, aplique cuatro espacios de sangría al carácter { en la definición de tipo, inicie la lista de etiquetas en la misma línea y alinee los miembros, si los hay, con el token {:

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

También es habitual preferir colocar corchetes en su propia línea y aplicar cuatro espacios más de sangría a las etiquetas:

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

También puede colocar el carácter { al final de la primera línea de la definición de tipo (estilo Stroustrup):

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

Si se necesitan miembros adicionales, no use with/end siempre que sea posible:

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

La excepción a esta regla de estilo es si aplica formato a los registros según el estilo Stroustrup. En esta situación, debido a las reglas del compilador, se requiere la palabra clave with si quiere implementar una interfaz o agregar más miembros:

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

Cuando se agrega documentación XML a los campos de registro, se prefiere el estilo Aligned o Stroustrup, y se deben agregar espacios en blanco adicionales entre los miembros:

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

Es preferible colocar el token de apertura en una nueva línea y el token de cierre en otra si declara implementaciones de interfaz o miembros en el registro:

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

Estas mismas reglas se aplican a los alias de tipo de registro anónimos.

Aplicación de formato a declaraciones de unión discriminadas

Para declaraciones de unión discriminadas, aplique cuatro espacios de sangría a | en la definición de tipo:

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

Cuando hay una sola unión corta, puede omitir el signo | inicial.

// ✔️ OK
type Address = Address of string

En el caso de una unión de varias líneas o más larga, mantenga el signo | y coloque cada campo de unión en una nueva línea, con la * de separación al final de cada línea.

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

Cuando se agreguen comentarios de documentación, use una línea vacía delante de cada comentario ///.

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

Aplicación de formato a declaraciones de literal

Los literales de F# que usan el atributo Literal deben colocar el atributo en su propia línea y usar la nomenclatura PascalCase:

// ✔️ OK

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

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

Evite colocar el atributo en la misma línea que el valor.

Aplicación de formato a declaraciones de módulo

Se debe aplicar sangría al código de un módulo local en relación con el módulo, pero no se debe aplicar sangría al código de un módulo de nivel superior. No es necesario aplicar sangría a los elementos de espacio de nombres.

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

Aplicación de formato a declaraciones do

En declaraciones de tipo, declaraciones de módulo y expresiones de cálculo, a veces es necesario el uso de do o do! para las operaciones de efecto colateral. Cuando estas abarcan varias líneas, use la sangría y una nueva línea para mantener la sangría coherente con let/let!. Este es un ejemplo de uso de do en una clase:

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

Este es un ejemplo con do! usando dos espacios de sangría (porque con do! no hay ninguna diferencia entre los enfoques al usar cuatro espacios de sangría):

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

Aplicación de formato a operaciones de expresión de cálculo

Al crear operaciones personalizadas para expresiones de cálculo, se recomienda usar la nomenclatura 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
    }

En última instancia, el dominio que se va a modelar debe determinar la convención de nomenclatura. Si es idiomático usar una convención diferente, esa convención debe usarse en su lugar.

Si el valor devuelto de una expresión es una expresión de cálculo, es preferible colocar el nombre de palabra clave de la expresión de cálculo en su propia línea:

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

También puede que prefiera colocar la expresión de cálculo en la misma línea que el nombre del enlace:

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

Sea cual sea su preferencia, su objetivo debe ser mantener la coherencia en todo el código base. Puede que los formateadores le permitan especificar esta preferencia para mantener la coherencia.

Aplicación de formato a tipos y anotaciones de tipo

En esta sección se describen los tipos de formato y las anotaciones de tipo. Se incluye el formato de los archivos de firma con la extensión .fsi.

En el caso de los tipos, se prefiere la sintaxis de prefijo para los genéricos (Foo<T>), con algunas excepciones específicas.

F# permite el estilo de postfijo de escribir tipos genéricos (por ejemplo, int list) y el estilo de prefijo (por ejemplo, list<int>). El estilo de postfijo solo se puede usar con un único argumento de tipo. Siempre se prefiere el estilo de .NET, excepto en el caso de cinco tipos específicos:

  1. Para las listas de F#, use la forma de postfijo: int list en lugar de list<int>.
  2. Para las opciones de F#, use la forma de postfijo: int option en lugar de option<int>.
  3. Para las opciones de valor de F#, use la forma de postfijo: int voption en lugar de voption<int>.
  4. Para las matrices F#, use el formato postfijo: int array en lugar de array<int> o int[].
  5. Para las celdas de referencia, use int ref en lugar de ref<int> o Ref<int>.

Para todos los demás tipos, use la forma de prefijo.

Aplicación de formato a tipos de función

Al definir la firma de una función, use espacios en blanco alrededor del símbolo ->:

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

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

Aplicación de formato a anotaciones de tipo de argumento y valor

Al definir valores o argumentos con anotaciones de tipo, use espacios en blanco después del símbolo :, pero no delante:

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

Aplicación de formato a las anotaciones de tipo multilínea

Cuando una anotación de tipo es larga o multilínea, colóquela en la siguiente línea, aplicando sangría en un nivel.

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

En el caso de los tipos de registros anónimos insertados, también puede usar el estilo Stroustrup:

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

Aplicación de formato a anotaciones de tipo de valor devuelto

En las anotaciones de tipo de valor devuelto de miembro o función, use espacios en blanco delante y detrás del símbolo ::

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

Aplicación de formato a tipos en firmas

Al escribir tipos de función completos en firmas, a veces es necesario dividir los argumentos en varias líneas. Siempre se aplica sangría al tipo de valor devuelto.

En una función en tupla, los argumentos están separados por el signo *, colocado al final de cada línea.

Por ejemplo, considere una función con la siguiente implementación:

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

En el archivo de firma correspondiente (extensión .fsi), se puede aplicar formato a la función de la siguiente manera cuando se requiere formato de varias líneas:

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

Del mismo modo, considere una función currificada:

let SampleCurriedFunction arg1 arg2 arg3 arg4 = ...

En el archivo de firma correspondiente, los signos -> se colocan al final de cada línea:

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

Del mismo modo, considere una función que toma una combinación de argumentos currificados y en tupla:

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

En el archivo de firma correspondiente, se aplica sangría a los tipos precedidos por una tupla.

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

Las mismas reglas se aplican a los miembros en las firmas de tipo:

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

Aplicación de formato a restricciones y argumentos de tipo genérico explícitos

Las instrucciones siguientes se aplican a las definiciones de función, las definiciones de miembro, las definiciones de tipo y las aplicaciones de función.

Mantenga los argumentos y restricciones de tipo genéricos en una sola línea si no es demasiado larga:

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

Si no caben los argumentos o restricciones de tipo genérico y las propiedades de función, pero los parámetros y restricciones de tipo sí, coloque los parámetros en nuevas líneas:

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

Si los parámetros o restricciones de tipo son demasiado largos, divídalos y alinéelos como se muestra a continuación. Mantenga la lista de parámetros de tipo en la misma línea que la función, independientemente de su longitud. En el caso de las restricciones, coloque when en la primera línea y mantenga cada restricción en una sola línea sin importar su longitud. Coloque > al final de la última línea. Aplique un nivel de sangría a las restricciones.

// ✔️ 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 se dividen los parámetros o restricciones de tipo, pero no hay parámetros de función normales, coloque el signo = en una nueva línea sin tener en cuenta lo siguiente:

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

Las mismas reglas se aplican a las aplicaciones de función:

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

Aplicación de formato a la herencia

Los argumentos del constructor de clase base aparecen en la lista de argumentos en la cláusula inherit. Coloque la cláusula inherit en una nueva línea, aplicando sangría en un nivel.

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)

Cuando el constructor es largo o multilínea, colóquelo en la siguiente línea, aplicando sangría en un nivel.
Aplique formato a este constructor multilínea según las reglas de las aplicaciones de funciones multilínea.

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

Aplicación de formato al constructor principal

En las convenciones de formato predeterminadas, no se agrega ningún espacio entre el nombre de tipo y los paréntesis del constructor 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

Varios constructores

Cuando la cláusula inherit forma parte de un registro, colóquela en la misma línea si es corta. Y colóquela en la siguiente línea, aplicando sangría en un nivel, si es larga o multilínea.

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 }

Aplicación de formato a atributos

Los atributos se colocan encima de una construcción:

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

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

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

Deben ir después de cualquier documentación XML:

// ✔️ OK

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

Aplicación de formato a atributos en parámetros

Los atributos también se pueden colocar en parámetros. En este caso, colóquelos en la misma línea que el parámetro y delante del nombre:

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

Aplicación de formato a varios atributos

Cuando se apliquen varios atributos a una construcción que no es un parámetro, coloque cada atributo en una línea distinta:

// ✔️ OK

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

Cuando se apliquen a un parámetro, coloque los atributos en la misma línea y sepárelos con un separador ;.

Agradecimientos

Estas directrices se basan en el documento Guía completa sobre las convenciones de formato de F# de Anh-Dung Phan.