Diretrizes de formatação de código do F#

Este artigo oferece diretrizes para formatar seu código para que seu código F# seja:

  • Mais legível
  • De acordo com as convenções aplicadas por ferramentas de formatação no Visual Studio Code e outros editores
  • Semelhante a outros códigos online

Consulte também as convenções de codificação e as diretrizes de design de componente, que também incluem convenções de nomenclatura.

Formatação automática de código

O formatador de código Fantomas é a ferramenta padrão da comunidade F# para formatação automática de código. As configurações padrão correspondem a este guia de estilo.

É altamente recomendável o uso desse formatador de código. Dentro das equipes do F#, as especificações de formatação de código devem ser acordadas e codificadas em termos de um arquivo de configurações acordado para o formatador de código verificado no repositório da equipe.

Regras gerais para formatação

O F# usa espaço em branco por padrão e diferencia espaço em branco. As diretrizes a seguir têm como objetivo fornecer instruções sobre como lidar com alguns desafios que podem ser impostos.

Use espaços e não guias

Quando o recuo é necessário, use espaços e não guias. O código F# não usa guias e o compilador gera um erro se um caractere de guia for encontrado fora de um literal de cadeia de caracteres ou comentário.

Usar recuo consistente

Ao recuar, pelo menos um espaço é necessário. Sua organização pode criar padrões de codificação para especificar o número de espaços a serem usados para recuo; são normais dois, três ou quatro espaços de recuo em cada nível de recuo.

Recomendamos quatro espaços por recuo.

Dito isto, o recuo de programas é um assunto subjetivo. As variações acontecem, mas a primeira regra que você deve seguir é a consistência do recuo. Escolha um estilo de recuo geralmente aceito e use-o sistematicamente em toda a base de código.

Evite formatação que seja sensível ao tamanho do nome

Procure evitar recuos e alinhamentos que sejam sensíveis à 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 _ -> ()
    | ...

Os principais motivos para evitar isso são:

  • Código importante é movido para a direita
  • Há menos largura para o código real
  • Renomear pode interromper o alinhamento

Evite espaço em branco desnecessário

Evite espaço em branco desnecessário no código F#, exceto quando descrito neste guia de estilo.

// ✔️ OK
spam (ham 1)

// ❌ Not OK
spam ( ham 1 )

Formatação de comentários

Prefira vários comentários de barra dupla em vez de comentários de bloco.

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

Os comentários devem ter a primeira letra maiúscula e ter frases bem formadas.

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

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

Para formatar comentários da documentação XML, consulte "Formatação de declarações" abaixo.

Formatação de expressões

Esta seção discute a formatação de expressões de diferentes tipos.

Formatação de expressões de cadeia de caracteres

Literais de cadeia de caracteres e cadeias de caracteres interpoladas podem ficar em uma única linha, independentemente do tamanho da linha.

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

Não são encorajadas expressões interpoladas de multilinhas. Em vez disso, associe o resultado da expressão a um valor e use-o na cadeia de caracteres interpolada.

Formatação de expressões de tupla

Uma instanciação de tupla deve estar entre parênteses e as vírgulas delimitantes dentro dela devem ser seguidas por um único espaço, por exemplo: (1, 2), (x, y, z).

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

É comumente aceito omitir parênteses nos padrões correspondentes de tuplas:

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

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

Também é comumente aceito omitir parênteses se a tupla for o valor retornado de uma função:

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

Em resumo, prefira instanciações de tupla entre parênteses, mas ao usar tuplas para padrões correspondentes ou um valor retornado, é bom evitar parênteses.

Formatação de expressões de aplicativo

Ao formatar um aplicativo de função ou método, os argumentos são fornecidos na mesma linha quando a largura da linha permite:

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

Omitir parênteses, a menos que os argumentos exijam:

// ✔️ OK
someFunction1 x.IngredientName

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

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

Não omitir espaços ao invocar vários argumentos na forma curried:

// ✔️ OK
someFunction1 (convertVolumeToLiter x) (convertVolumeUSPint x)
someFunction2 (convertVolumeToLiter y) y
someFunction3 z (convertVolumeUSPint z)

// ❌ Not preferred - spaces should not be omitted between arguments
someFunction1(convertVolumeToLiter x)(convertVolumeUSPint x)
someFunction2(convertVolumeToLiter y) y
someFunction3 z(convertVolumeUSPint z)

Em convenções de formatação padrão, um espaço é adicionado ao aplicar funções minúsculas a argumentos em tupla ou entre parênteses (mesmo quando um único argumento é usado):

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

Em convenções de formatação padrão, nenhum espaço é adicionado ao aplicar métodos com letras maiúsculas a argumentos de tupla. Isso ocorre porque geralmente são usados na programação 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)

Talvez seja necessário aprovar argumentos para uma função em uma nova linha como uma questão de legibilidade ou porque a lista de argumentos ou os nomes dos argumentos são muito longos. Nesse caso, recuou um nível:

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

// ✔️ OK
someFunction3
    x.IngredientName1 x.Quantity2
    x.IngredientName2 x.Quantity2

// ✔️ OK
someFunction4
    x.IngredientName1
    x.Quantity2
    x.IngredientName2
    x.Quantity2

// ✔️ OK
someFunction5
    (convertVolumeToLiter x)
    (convertVolumeUSPint x)
    (convertVolumeImperialPint x)

Quando a função usa um único argumento em tupla de multilinhas, coloque cada argumento em uma nova linha:

// ✔️ OK
someTupledFunction (
    478815516,
    "A very long string making all of this multi-line",
    1515,
    false
)

// OK, but formatting tools will reformat to the above
someTupledFunction
    (478815516,
     "A very long string making all of this multi-line",
     1515,
     false)

Se as expressões de argumento forem curtas, separe os argumentos com espaços e mantenha-os em uma linha.

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

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

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

Se as expressões de argumento forem longas, use novas linhas e recuo em um nível, em vez de recuar para o parêntese esquerdo.

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

As mesmas regras se aplicam mesmo que haja apenas um argumento de multilinhas, incluindo cadeias de caracteres de multilinhas:

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

Formatação de expressões de pipeline

Ao usar várias linhas, os operadores de pipeline |> devem estar debaixo das expressões operadas.

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

Formatação de expressões lambda

Quando uma expressão lambda é usada como um argumento em uma expressão de multilinhas e é seguida por outros argumentos, coloque o corpo de uma expressão lambda em uma nova linha, recuada por um nível:

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

Se o argumento lambda for o último argumento em um aplicativo de funções, coloque todos os argumentos até a seta na mesma linha.

// ✔️ 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 a expressão lambda correspondente de forma semelhante.

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

Quando há muitos argumentos nas entrelinhas ou multilinhas antes do recuo lambda, todos os argumentos com um nível.

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

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

Se o corpo de uma expressão lambda tiver várias linhas de comprimento, considere a refatoração em uma função com escopo local.

Quando os pipelines incluem expressões lambda, cada expressão lambda normalmente é o último argumento em cada fase do 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}")

Caso os argumentos de um lambda não caibam em uma única linha ou sejam multilinha, coloque-os na próxima linha, recuados por um nível.

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

Formatação de expressões aritméticas e binárias

Sempre use espaço em branco em torno de expressões aritméticas binárias:

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

Falha ao cercar um operador binário -, quando combinado com determinadas opções de formatação, pode levar a interpretá-lo como um unário -. Os operadores unários - sempre devem ser seguidos imediatamente pelo valor negado:

// ✔️ OK
let negate x = -x

// ❌ Not OK
let negateBad x = - x

A adição de um caractere de espaço em branco após o - operador pode causar confusão para outras pessoas.

Separe os operadores binários por espaços. As expressões de infixo estão OK para a formação na mesma coluna:

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

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

Essa regra também se aplica a unidades de medidas em tipos e anotações 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)> }

Os operadores a seguir são definidos na biblioteca padrão F# e devem ser usados em vez de definir equivalentes. O uso desses operadores é recomendado, pois ele tende a tornar o código mais legível e idiomático. A lista a seguir resume os operadores 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

Formatação de expressões de operador de intervalo

Adicione apenas espaços ao redor de .. quando todas as expressões não são atômicas. Números inteiros e identificadores de palavra única são considerados 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 ]

Essas regras também se aplicam à divisão:

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

Formatação de expressões

O recuo das condicionais depende do tamanho e da complexidade das expressões que as compõem. Escreva-os em uma linha quando:

  • cond, e1e e2 são curtos.
  • e1 e e2 não if/then/else são expressões em si.
// ✔️ OK
if cond then e1 else e2

Se a outra expressão estiver ausente, é recomendável nunca gravar a expressão inteira em uma linha. Isso é para diferenciar o código imperativo do funcional.

// ✔️ OK
if a then
    ()

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

Se qualquer uma das expressões for de várias linhas, cada branch condicional deverá ser de várias linhas.

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

Várias condicionais com elif e else são recuadas no mesmo escopo que quando if seguem as regras das expressões de uma linha if/then/else.

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

Se qualquer uma das condições ou expressões for multilinha, toda a expressão if/then/else será multilinha:

// ✔️ OK
if cond1 then
    let e1 = something()
    e1
elif cond2 then
    e2
elif cond3 then
    e3
else
    e4

// ❌ Not OK
if cond1 then
    let e1 = something()
    e1
elif cond2 then e2
elif cond3 then e3
else e4

Se uma condição for multilinha ou exceder a tolerância padrão da linha única, a expressão de condição deverá usar um recuo e uma nova linha. A palavra-chave if e then devem estar alinhadas ao encapsular a expressão de condição longa.

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

No entanto, é um estilo melhor para refatorar condições longas para uma função de associação ou de separação de let:

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

if performAction then
    e1
else
    e2

Formatação de expressões de caso de união

A aplicação de casos de união discriminados segue as mesmas regras que os aplicativos de função e método. Ou seja, como o nome é colocado em maiúscula, os formatadores de código removerão um espaço antes de uma tupla:

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

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

Assim como os aplicativos de funções, as construções que se dividem entre multilinhas devem usar recuo:

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

Formatação de expressões de lista e matriz

Gravação x :: l com espaços ao redor do operador :: (:: é um operador infixo, portanto, cercado por espaços).

A lista e as matrizes declaradas em uma única linha devem ter um espaço após o colchete de abertura e antes do colchete de fechamento:

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

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

Sempre use pelo menos um espaço entre dois operadores distintos semelhantes a chaves. Por exemplo, deixe um espaço entre um [ e um {.

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

A mesma diretriz se aplica a listas ou matrizes de tuplas.

Listas e matrizes que se dividem entre multilinhas seguem uma regra semelhante à dos 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 |] |]

Assim como acontece com os registros, declarar os colchetes de abertura e fechamento em sua própria linha facilita a movimentação de código e tubulação em funções:

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

Se uma expressão de lista ou matriz for o lado direito de uma associação, você poderá preferir usar o 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 |] 
|]

No entanto, quando uma expressão de lista ou matriz não é o lado direito de uma associação, como quando está dentro de outra lista ou matriz, se essa expressão interna precisar abranger várias linhas, os colchetes deverão ir em suas próprias linhas:

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

A mesma regra se aplica a tipos de registro dentro de matrizes/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
} ]

Ao gerar matrizes e listas programaticamente, prefira -> do que do ... yield quando um valor é sempre gerado:

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

Versões mais antigas do F# exigiam especificação yield em situações em que os dados podem ser gerados condicionalmente ou pode haver expressões consecutivas a serem avaliadas. Prefira omitir essas palavras-chave yield, a menos que você precise compilar uma versão mais antiga da linguagem 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"
    ]

Em alguns casos, do...yield pode ajudar na legibilidade. Esses casos, embora subjetivos, devem ser levados em consideração.

Formatação de expressões de registro

Registros curtos podem ser gravados em uma linha:

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

Os registros mais longos devem usar novas linhas para rótulos:

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

Estilos de formatação de colchetes multilinha

Para registros que abrangem várias linhas, há três estilos de formatação comumente usados: Cramped, Aligned e Stroustrup. O estilo Cramped tem sido o padrão para o código F#, pois tende a favorecer estilos que permitem que o compilador analise facilmente o código. Os estilos Aligned e Stroustrup permitem a reordenação mais fácil de membros, levando a um código que pode ser mais fácil de refatorar, com a desvantagem de que determinadas situações podem exigir um código um pouco mais detalhado.

  • Cramped: o padrão histórico e o formato de registro F# padrão. Os colchetes de abertura ficam na mesma linha que o primeiro membro, fechando o colchete na mesma linha que o último membro.

    let rainbow = 
        { Boss1 = "Jeffrey"
          Boss2 = "Jeffrey"
          Boss3 = "Jeffrey"
          Lackeys = [ "Zippy"; "George"; "Bungle" ] }
    
  • Aligned: cada um dos colchetes obtém a própria linha, alinhada na mesma coluna.

    let rainbow =
        {
            Boss1 = "Jeffrey"
            Boss2 = "Jeffrey"
            Boss3 = "Jeffrey"
            Lackeys = ["Zippy"; "George"; "Bungle"]
        }
    
  • Stroustrup: o colchete de abertura fica na mesma linha que a associação, o colchete de fechamento obtém a própria linha.

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

As mesmas regras de estilo de formatação se aplicam a elementos de lista e matriz.

Formatação Copiar e Atualizar expressões de registro

Uma expressão de registro de copiar e atualizar ainda é um registro, portanto, são aplicadas diretrizes semelhantes.

Expressões curtas podem caber em uma linha:

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

Expressões mais longas devem usar novas linhas e formatar com base em uma das convenções nomeadas acima:

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

Observação: se estiver usando o estilo Stroustrup para expressões de cópia e atualização, você precisará recuar membros além do nome do 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" 
}

Formatação de padrões correspondentes

Use | para cada cláusula de uma correspondência sem recuo. Se a expressão for curta, você poderá considerar o uso de uma única linha se cada subexpressão também for simples.

// ✔️ OK
match l with
| { him = x; her = "Posh" } :: tail -> x
| _ :: tail -> findDavid tail
| [] -> failwith "Couldn't find David"

// ❌ Not OK, code formatters will reformat to the above by default
match l with
    | { him = x; her = "Posh" } :: tail -> x
    | _ :: tail -> findDavid tail
    | [] -> failwith "Couldn't find David"

Se a expressão à direita da seta de padrões correspondentes for muito grande, mova-a para a linha seguinte, recuada a uma etapa de match/|.

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

Semelhante às condições grandes, se uma expressão de correspondência for multilinha ou exceder a tolerância padrão da linha única, a expressão de correspondência deverá usar um recuo e uma nova linha. A palavra-chave match e with devem estar alinhadas ao encapsular a expressão de correspondência longa.

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

No entanto, é um estilo melhor para refatorar expressões de correspondência longa para uma função de associação ou de separação de let:

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

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

O alinhamento das setas de uma correspondência padrão deve ser evitado.

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

Os padrões correspondentes introduzidos por uma palavra-chave function devem recuar um nível desde o início da linha anterior:

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

O uso de function nas funções definidas por let ou let rec deve ser evitado em geral em favor de um match. Se usadas, as regras de padrão devem estar alinhadas à palavra-chave 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

Formatação de try/com expressões

Os padrões correspondentes no tipo de exceção devem ser recuados no mesmo nível 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"

Adicione uma | para cada cláusula, exceto quando houver apenas uma única 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

Formatação de argumentos nomeados

Os argumentos nomeados devem ter espaços próximo de =:

// ✔️ OK
let makeStreamReader x = new System.IO.StreamReader(path = x)

// ❌ Not OK, spaces are necessary around '=' for named arguments
let makeStreamReader x = new System.IO.StreamReader(path=x)

Quando os padrões correspondentes usando uniões discriminadas, os padrões nomeados são formatados da mesma forma, por exemplo.

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

Formatação de expressões de mutação

As expressões de mutação location <- expr normalmente são formatadas em uma linha. Se a formatação de multilinhas for necessária, coloque a expressão do lado direito em uma nova linha.

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

Formatação de expressões de objeto

Os membros da expressão de objeto devem ser alinhados com member, tendo o recuo por um nível.

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

Você também pode preferir usar o 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)
}

Definições de tipo vazias podem ser formatadas em uma linha:

type AnEmptyType = class end

Independentemente da largura da página escolhida, = class end deve estar sempre na mesma linha.

Formatação de expressões de índice/fatia

As expressões de índice não devem conter espaços ao redor dos colchetes de abertura e fechamento.

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

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

Isso também se aplica à sintaxe expr.[idx] mais antiga.

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

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

Formatação de expressões entre aspas

Os símbolos delimitadores (<@ , @>, <@@, @@>) devem ser colocados em linhas separadas se a expressão entre aspas for uma expressão de multilinhas.

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

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

Em expressões de linha única, os símbolos delimitadores devem ser colocados na mesma linha que a própria expressão.

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

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

Formatação de expressões encadeadas

Quando as expressões encadeadas (aplicativos de função entrelaçados com .) forem longas, coloque cada invocação de aplicativo na próxima linha. Recue os links subsequentes na cadeia por um nível após o link principal.

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

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

O link à esquerda poderá ser composto por vários links se eles forem identificadores simples. Por exemplo, a adição de um namespace totalmente qualificado.

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

Os links subsequentes também devem conter 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))

Quando os argumentos dentro de um aplicativo de funções não se ajustarem ao restante da linha, coloque cada argumento na próxima linha.

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

Os argumentos lambda dentro de um aplicativo de funções devem começar na mesma linha que o ( de abertura.

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

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

Formatação de declarações

Esta seção discute a formatação de declarações de diferentes tipos.

Adicionar linhas em branco entre declarações

Separe as definições de função e classe de nível superior com uma única linha em branco. Por exemplo:

// ✔️ OK
let thing1 = 1+1

let thing2 = 1+2

let thing3 = 1+3

type ThisThat = This | That

// ❌ Not OK
let thing1 = 1+1
let thing2 = 1+2
let thing3 = 1+3
type ThisThat = This | That

Se um constructo tiver comentários da documentação XML, adicione uma linha em branco antes do comentário.

// ✔️ OK

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

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

Formatação de declarações de let e membro

Ao formatar declarações let e member, normalmente o lado direito de uma associação fica em uma linha ou (se for muito longo) entra em um novo nível recuado de uma linha.

Por exemplo, os seguintes exemplos são compatíveis:

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

Estes não são compatíveis:

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

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

As instanciações de tipo de registro também podem colocar os colchetes nas suas próprias linhas:

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

Você também pode preferir usar o estilo Stroustrup, com o { de abertura na mesma linha que o nome da associação:

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

Separe membros com uma única linha em branco e um documento e adicione um comentário de documentação:

// ✔️ OK

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

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

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

Linhas em branco extras podem ser usadas (com moderação) para separar grupos de funções relacionadas. Linhas em branco podem ser omitidas entre um monte de linhas únicas relacionadas (por exemplo, um conjunto de implementações fictícias). Use linhas em branco em funções, com moderação, para indicar seções lógicas.

Formatação de argumentos de função e membro

Ao definir uma função, use o espaço em branco em torno 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

Se você tiver uma definição de função longa, coloque os parâmetros em novas linhas e recue-os para corresponder ao nível de recuo do parâmetro subsequente.

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

Também se aplica a membros, construtores e parâmetros usando 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

Se os parâmetros forem curried, coloque o caractere = junto com qualquer tipo de retorno em uma nova linha:

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

Essa é uma forma de evitar linhas muito longas (caso o tipo de retorno possa ter nome longo) e menos danos de linha ao adicionar parâmetros.

Formatação de declarações do operador

Em alternativa, use espaço em branco para cercar uma definição de operador:

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

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

Para qualquer operador personalizado que comece com * e tenha mais de um caractere, você precisa adicionar um espaço em branco ao início da definição para evitar uma ambiguidade do compilador. Por isso, recomendamos que você simplesmente envolva as definições de todos os operadores com um único caractere de espaço em branco.

Formatação de declarações de registro

Para declarações de registro, por padrão, você deve recuar o { na definição de tipo em quatro espaços, iniciar a lista de rótulos na mesma linha e alinhar membros, se houver, com o token {:

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

Também é comum preferir colocar colchetes em sua própria linha, com rótulos recuados por mais quatro espaços:

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

Você também pode colocar o { no final da primeira linha da definição de tipo (estilo Stroustrup):

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

Se membros adicionais forem necessários, não use with/end sempre que possível:

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

A exceção a essa regra de estilo será se você formatar registros de acordo com o estilo Stroustrup. Nessa situação, devido às regras do compilador, a palavra-chave with será necessária se você quiser implementar uma interface ou adicionar membros adicionais:

// ✔️ OK
type PostalAddress = {
    Address: string
    City: string
    Zip: string
} with
    member x.ZipAndCity = $"{x.Zip} {x.City}"
   
// ❌ Not OK, this is currently invalid F# code
type PostalAddress = {
    Address: string
    City: string
    Zip: string
} 
member x.ZipAndCity = $"{x.Zip} {x.City}"

Quando a documentação XML é adicionada para campos de registro, o estilo Aligned ou Stroustrup é preferencial, e um espaço em branco adicional deve ser adicionado entre os membros:

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

É recomendável colocar o token de abertura em uma nova linha e o token de fechamento em uma nova linha se você declarar implementações de interface ou membros no 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

Essas mesmas regras se aplicam a aliases de tipo de registro anônimo.

Formatação de declarações de união discriminadas

Para declarações de união discriminadas, faça o recuo | na definição de tipo em quatro espaços:

// ✔️ OK
type Volume =
    | Liter of float
    | FluidOunce of float
    | ImperialPint of float

// ❌ Not OK
type Volume =
| Liter of float
| USPint of float
| ImperialPint of float

Quando há uma única união curta, você pode omitir o entrelinhamento |.

// ✔️ OK
type Address = Address of string

Para uma união mais longa ou multilinhas, mantenha o | e coloque cada campo de união em uma nova linha, com a separação de * no final de cada linha.

// ✔️ OK
[<NoEquality; NoComparison>]
type SynBinding =
    | SynBinding of
        accessibility: SynAccess option *
        kind: SynBindingKind *
        mustInline: bool *
        isMutable: bool *
        attributes: SynAttributes *
        xmlDoc: PreXmlDoc *
        valData: SynValData *
        headPat: SynPat *
        returnInfo: SynBindingReturnInfo option *
        expr: SynExpr *
        range: range *
        seqPoint: DebugPointAtBinding

Quando os comentários da documentação forem adicionados, use uma linha vazia antes de cada comentário ///.

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

Formatação de declarações literais

OsLiterais F# usando o atributo Literal devem colocar o atributo em sua própria linha e usar a nomenclatura PascalCase:

// ✔️ OK

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

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

Evite colocar o atributo na mesma linha que o valor.

Formatação de declarações de módulo

O código em um módulo local deve ser recuado em relação ao módulo, mas o código em um módulo de nível superior não deve ser recuado. Os elementos de namespace não precisam ser recuados.

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

Formatação de declarações

Em declarações de tipo, declarações de módulo e expressões de computação, o uso de do ou do!, às vezes, é necessário para operações com efeito colateral. Quando elas incluem multilinhas, use o recuo e uma nova linha para manter o recuo de acordo com let/let!. Veja um exemplo usando do em uma 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

Veja aqui um exemplo com do! usando dois espaços de recuo (porque, do! coincidentemente, não há diferença entre as abordagens ao usar quatro espaços de recuo):

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

Formatação de operações de expressão de computação

Ao criar operações personalizadas para expressões de computação, é recomendável usar a 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
    }

O domínio que está sendo preparado deve, em última análise, conduzir a convenção de nomenclatura. Se for idioma usar uma convenção diferente, essa convenção deverá ser usada em seu lugar.

Se o valor retornado de uma expressão for uma expressão de computação, prefira colocar o nome da palavra-chave da a expressão de computação na própria linha:

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

Você também pode preferir colocar a expressão de computação na mesma linha que o nome da associação:

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

Independentemente de sua preferência, você deve ter como objetivo permanecer consistente em toda a base de código. Os formatadores podem permitir que você especifique essa preferência para permanecer consistente.

Formatação de tipos e anotações de tipo

Esta seção discute tipos de formatação e anotações de tipo. Isso inclui a formatação de arquivos de assinatura com a extensão .fsi.

Para tipos, prefira a sintaxe de prefixo para genéricos (Foo<T>), com algumas exceções específicas

O F# permite o estilo de sufixo de escrever tipos genéricos (por exemplo, int list)e o estilo de prefixo (por exemplo, list<int>). O estilo de sufixo somente pode ser usado com um argumento de tipo único. Sempre prefira o estilo .NET, exceto para cinco tipos específicos:

  1. Para listas F#, use o formulário de sufixo: int list em vez de list<int>.
  2. Para opções F#, use o formulário de sufixo: int option em vez de option<int>.
  3. Para opções de valor F#, use o formulário de sufixo: int voption em vez de voption<int>.
  4. Para matrizes F#, use o formulário de sufixo: int array em vez de array<int> ou int[].
  5. Para células de referência, use int ref ao invés de ref<int> ou Ref<int>.

Para todos os outros tipos, use o formulário de prefixo.

Formatação de tipos de função

Ao definir a assinatura de uma função, use o espaço em branco ao redor do símbolo ->:

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

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

Anotação de valor de formatação e anotações de tipo de argumento

Ao definir valores ou argumentos com anotações de tipo, use espaço em branco após o símbolo :, mas não antes:

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

Formatação de anotações de tipo de multilinha

Quando uma anotação de tipo for longa ou multilinha, coloque-as na próxima linha, recuada por um nível.

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

Para tipos de registro anônimo embutidos, você também pode usar o estilo Stroustrup:

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

Formatação de anotações de tipo de retorno

Nas anotações de tipo de retorno de função ou membro, use espaço em branco antes e depois do 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

Formatação de tipos de assinatura

Ao escrever tipos de função completos em assinaturas, às vezes, é necessário dividir os argumentos em multilinhas. O tipo de retorno é sempre recuado.

Para uma função tupled, os argumentos são separados por *, colocados no final de cada linha.

Por exemplo, considere uma função com a seguinte implementação:

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

No arquivo de assinatura correspondente (extensão .fsi), a função pode ser formatada da seguinte maneira quando é necessária a formatação de multilinhas:

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

Da mesma forma, considere uma função curried:

let SampleCurriedFunction arg1 arg2 arg3 arg4 = ...

No arquivo de assinatura correspondente, os -> são colocados no final de cada linha:

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

Da mesma forma, considere uma função que usa uma combinação de argumentos curried e tupled:

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

No arquivo de assinatura correspondente, os tipos precedidos por uma tupla são recuados

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

As mesmas regras se aplicam a membros em assinaturas de tipo:

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

Formatação de restrições e argumentos genéricos explícitos

As diretrizes abaixo se aplicam a definições de função, definições de membro, definições de tipo e aplicativos de função.

Mantenha argumentos de tipo genérico e restrições em uma única linha se não forem muito longos:

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

Se ambos os argumentos/restrições de tipo genéricos e parâmetros de função não se ajustarem, mas se os parâmetros/restrições de tipo fizerem, coloque os parâmetros em novas linhas:

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

Se os parâmetros ou restrições de tipo forem muito longos, quebre e alinhe-os conforme mostrado abaixo. Mantenha a lista de parâmetros de tipo na mesma linha que a função, independentemente de seu comprimento. Para restrições, coloque when na primeira linha e mantenha cada restrição em uma única linha, independentemente de seu comprimento. Coloque > no final da última linha. Recue as restrições em um nível.

// ✔️ OK
let inline f< ^T1, ^T2
    when ^T1: (static member Foo1: unit -> ^T2)
    and ^T2: (member Foo2: unit -> int)
    and ^T2: (member Foo3: string -> ^T1 option)>
    arg1
    arg2
    =
    // function body

Se os parâmetros/restrições de tipo forem divididos, mas não houver parâmetros de função normais, coloque = em uma nova linha, independentemente:

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

As mesmas regras se aplicam a aplicativos de funções:

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

Herança de formatação

Os argumentos para o construtor de classe base aparecem na lista de argumentos na cláusula inherit. Coloque a cláusula inherit em uma nova linha, recuada por um nível.

type MyClassBase(x: int) =
   class
   end

// ✔️ OK
type MyClassDerived(y: int) =
   inherit MyClassBase(y * 2)

// ❌ Not OK
type MyClassDerived(y: int) = inherit MyClassBase(y * 2)

Quando um constructo for longo ou multilinha, coloque-os na próxima linha, recuada por um nível.
Formate esse constructo multilinha de acordo com as regras de aplicativos de funções multilinha.

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

Formatando o construtor primário

Em convenções de formatação padrão, nenhum espaço é adicionado entre o nome do tipo e os parênteses do construtor primário.

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

Vários constructos

Quando a cláusula inherit fizer parte de um registro, coloque-a na mesma linha se for curta. E coloque-a na próxima linha, recuada por um nível, se for longa ou multilinha.

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 }

Formatação de atributos

Os atributos são colocados acima de um constructo:

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

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

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

Eles devem ir atrás de qualquer documentação XML:

// ✔️ OK

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

Formatação de atributos nos parâmetros

Os atributos também podem ser colocados em parâmetros. Nesse caso, coloque então na mesma linha que o parâmetro e antes do nome:

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

Formatação de vários atributos

Quando vários atributos são aplicados a um constructo que não é um parâmetro, coloque cada atributo em uma linha separada:

// ✔️ OK

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

Quando aplicado a um parâmetro, coloque os atributos na mesma linha e separe-os com um separador ;.

Agradecimentos

Essas diretrizes são baseadas em um guia abrangente das Convenções de Formatação F# por Anh-Dung Phan.