Бөлісу құралы:


Рекомендации по форматированию кода F#

В этой статье приведены рекомендации по форматированию кода таким образом, чтобы код F# был следующим:

  • Более читаемый
  • В соответствии с соглашениями, применяемыми средствами форматирования в Visual Studio Code и другими редакторами
  • Аналогично другому коду в Сети

См. также соглашения о кодировании и рекомендации по проектированию компонентов, которые также охватывают соглашения об именовании.

Автоматическое форматирование кода

Средство форматирования кода Fantomas — это стандартное средство сообщества F# для автоматического форматирования кода. Параметры по умолчанию соответствуют этому руководству по стилю.

Настоятельно рекомендуется использовать этот модуль форматирования кода. В командах F# спецификации форматирования кода должны быть согласованы и закреплены в файле настроек для инструмента форматирования кода, проверенного в репозитории команды.

Общие правила форматирования

F# использует значимые пробелы по умолчанию и чувствителен к пробелам. Следующие рекомендации предназначены для того, чтобы помочь справляться с вызовами, которые это может повлечь.

Используйте пробелы, не вкладки

Если отступ требуется, необходимо использовать пробелы, а не вкладки. Код F# не использует вкладки, и компилятор даст ошибку, если символ вкладки обнаружен за пределами строкового литерала или комментария.

Используйте согласованный отступ

При отступе требуется по крайней мере одно пространство. Ваша организация может создавать стандарты кодирования, чтобы указать количество пробелов, используемых для отступа; два, три или четыре пробела на каждом уровне, где происходит отступ, это обычно.

Рекомендуем использовать четыре пробела в отступе.

Тем не менее, отступы в программах являются субъективным. Варианты являются ОК, но первое правило, которое следует следовать, — согласованность отступа. Выберите общепринятый стиль отступов и используйте его систематически во всей кодовой базе.

Избегайте форматирования, чувствительного к длине имени

Старайтесь избегать отступа и выравнивания, которые чувствительны к именованию:

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

Основными причинами для предотвращения этого являются следующие:

  • Важный код перемещен далеко вправо
  • Осталось меньше места для самого кода
  • Переименование может нарушить выравнивание

Избегайте лишних пробелов

Избегайте лишних пробелов в коде F#, за исключением случаев, описанных в этом руководстве по стилю.

// ✔️ OK
spam (ham 1)

// ❌ Not OK
spam ( ham 1 )

Форматирование комментариев

Предпочитайте несколько комментариев с двумя косыми чертами вместо блочных комментариев.

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

Комментарии должны начинаться с заглавной буквы и быть правильными фразами или предложениями.

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

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

Сведения о форматировании комментариев XML-документа см. в разделе "Форматирование деклараций" ниже.

Выражения форматирования

В этом разделе рассматриваются выражения форматирования различных типов.

Форматирование строковых выражений

Строковые литералы и интерполированные строки можно просто оставить в одной строке, независимо от длительности строки.

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

Интерполяцию выражений в нескольких строках не приветствуется. Вместо этого привязать результат выражения к значению и использовать его в интерполированной строке.

Форматирование выражений кортежей

Экземпляр кортежа должен быть в круглых скобках, а запятые в нем должны быть отделены одним пробелом, например: (1, 2), (x, y, z).

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

Обычно принято опустить скобки в сопоставлении шаблонов кортежей:

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

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

Кроме того, обычно принято пропускать скобки, если кортеж является возвращаемым значением функции:

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

В итоге, предпочитайте заключать кортежи в круглые скобки, но если они используются для сопоставления шаблонов или в качестве возвращаемого значения, считается допустимым обойтись без скобок.

Форматирование выражений приложения

При форматировании приложения функции или метода аргументы предоставляются в той же строке, когда разрешена ширина строки:

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

Не указывайте круглые скобки, если аргументы не требуют их:

// ✔️ OK
someFunction1 x.IngredientName

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

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

Не пропускайте пробелы при вызове с несколькими каррированными аргументами.

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

В соглашениях о форматировании по умолчанию пространство добавляется при применении функций нижнего регистра к кортежным или круглым аргументам (даже при использовании одного аргумента):

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

В соглашениях о форматировании по умолчанию пробел не добавляется при применении прописных методов к кортежным аргументам. Это связано с тем, что они часто используются при флюентном программировании:

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

Эти же соглашения о форматировании применяются к сопоставлению шаблонов. Согласованное форматирование значений стиля F#

// ✔️ OK - Consistent formatting for expressions and patterns
let result = Some(value)

match result with
| Some(x) -> x
| None -> 0

Возможно, потребуется передать аргументы функции в новой строке в качестве значения удобочитаемости или потому, что список аргументов или имена аргументов слишком длинны. В этом случае добавьте отступ одного уровня:

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

Когда функция принимает один многострочный кортежный аргумент, поместите каждый аргумент в новую строку:

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

Если выражения аргументов коротки, разделите аргументы пробелами и сохраните его в одной строке.

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

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

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

Если выражения аргументов длинны, используйте новые линии и отступ одного уровня, а не отступ в левой скобке.

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

Те же правила применяются, даже если существует только один многострочный аргумент, включая многострочные строки:

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

Форматирование выражений конвейера

При использовании нескольких строк операторы конвейера |> должны располагаться под выражениями, с которыми они работают.

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

Для операторов обратного конвейера <| сохраняйте короткие выражения в одной строке. Если длина строки требует упаковки, поместите аргументы в новые строки и последовательно выравнивайте их:

// ✔️ OK - short expressions stay on one line
let result = someFunction <| arg1 <| arg2 <| arg3

// ✔️ OK - longer expressions can wrap when necessary
failwith
<| sprintf "A very long error message that exceeds reasonable line length: %s - additional details: %s"
    longVariableName
    anotherLongVariableName

// ✔️ OK - align continuation lines with the operator
let longResult =
    someVeryLongFunctionName
    <| firstVeryLongArgumentName
    <| secondVeryLongArgumentName
    <| thirdVeryLongArgumentName

// ❌ Not OK - unnecessary wrapping of short expressions
failwith <| sprintf "short: %s"
                    value

Форматирование лямбда-выражений

Если лямбда-выражение используется в качестве аргумента в многостроковом выражении и после него следуют другие аргументы, поместите тело лямбда-выражения на новую строку, отступив на один уровень.

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

Если лямбда-аргумент является последним аргументом в приложении-функции, поместите все аргументы до стрелки в одной строке.

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

Относитесь к match-лямбда аналогичным образом.

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

При наличии большого количества ведущих или многострочных аргументов перед лямбда-функцией, все аргументы необходимо выравнивать на один уровень.

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

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

Если тело лямбда-выражения состоит из нескольких строк, следует рассмотреть возможность рефакторинга в локальную функцию.

Если конвейеры включают лямбда-выражения, каждое лямбда-выражение обычно является последним аргументом на каждом этапе конвейера:

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

Если аргументы лямбда-функции не помещаются в одну строку или являются многострочными, поместите их на следующую строку, отступив на один уровень.

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

Форматирование отложенных выражений

При написании однострочных ленивых выражений размещайте все в одной строке.

// ✔️ OK
let x = lazy (computeValue())

// ✔️ OK  
let y = lazy (a + b)

Для многострочных отложенных выражений поместите открывающую скобку на той же строке, что и ключевое слово lazy, при этом тело выражения отступите на один уровень, а закрывающая скобка выровнена с открывающей.

// ✔️ OK
let v =
    lazy (
        // some code
        let x = computeExpensiveValue()
        let y = computeAnotherValue()
        x + y
    )

// ✔️ OK
let handler =
    lazy (
        let connection = openConnection()
        let data = fetchData connection
        processData data
    )

Это следует тому же шаблону, что и другие приложения-функции с многостроными аргументами. Открывающая скобка остается с lazy, и выражение смещается на один уровень.

Форматирование арифметических и двоичных выражений

Всегда используйте пробелы вокруг двоичных арифметических выражений:

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

Отсутствие окружения двоичного - оператора при сочетании с определенным выбором форматирования может привести к интерпретации его как унарного -. Унарные - операторы всегда должны быть немедленно следуют значению, которое они отменяют:

// ✔️ OK
let negate x = -x

// ❌ Not OK
let negateBad x = - x

Добавление пробела после оператора - может привести к путанице для других.

Разделение двоичных операторов по пробелам. Выражения Infix являются ОК для формирования строки в одном столбце:

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

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

Это правило также применяется к единицам измерения в типах и константных аннотациях.

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

Следующие операторы определены в стандартной библиотеке F# и должны использоваться вместо определения эквивалентов. Использование этих операторов рекомендуется, так как он, как правило, делает код более читаемым и идиоматичным. В следующем списке приведены рекомендуемые операторы F#.

// ✔️ OK
x |> f // Forward pipeline
f <| x // Reverse 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

Форматирование выражений операторов диапазона

Добавляйте пробелы только в .. случае, если любое выражение не является атомарным. Целые числа и идентификаторы одного слова считаются атомарными.

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

Эти правила также применяются к срезу:

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

Форматирование условных выражений

Отступы условных условий зависят от размера и сложности выражений, составляющих их. Напишите их в одной строке, когда:

  • cond, e1и e2 короткие.
  • e1 и e2 сами по себе не являются выражениями if/then/else.
// ✔️ OK
if cond then e1 else e2

Если другое выражение отсутствует, рекомендуется никогда не записывать все выражение в одной строке. Это позволяет различать императивный код от функционального.

// ✔️ OK
if a then
    ()

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

Если любое из выражений является многострочным, каждая условная ветвь должна быть многострочной.

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

Несколько условных операторов с elif и else отступлены на том же уровне, что и if, при соблюдении правил однострочных выражений if/then/else.

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

Если хоть одно из условий или выражений является многострочным, то все if/then/else выражения являются многострочными.

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

Если условие многострочное или превышает допустимый предел для одиночной строки, выражение условия должно использовать один отступ и новую строку. Ключевое if слово должно быть выравнено при инкапсуляции длинного условия выражения.

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

Лучше изменить стиль, рефакторизуя длинные условия в let-биндинг или отдельную функцию.

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

if performAction then
    e1
else
    e2

Форматирование выражений варианта объединения

Применение случаев дискриминированных объединений соответствует тем же правилам, что и применение функций и методов. Это связано с тем, что имя написано с заглавной буквы, поэтому форматировщики кода удаляют пробел перед кортежем.

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

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

Как и применение функций, конструкции, разделённые на несколько строк, должны использовать отступ:

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

Форматирование выражений списка и массива

Напишите x :: l с пробелами вокруг :: оператора (:: — инфиксный оператор, поэтому окружённый пробелами).

Список и массивы, объявленные в одной строке, должны иметь пробел после открывающей скобки и перед закрывающей скобкой:

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

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

Всегда используйте по крайней мере один пробел между двумя отдельными операторами, похожими на скобки. Например, оставьте пробел между a [ и a {.

// ✔️ OK
[ { Ingredient = "Green beans"; Quantity = 250 }
  { Ingredient = "Pine nuts"; Quantity = 250 }
  { Ingredient = "Feta cheese"; Quantity = 250 }
  { Ingredient = "Olive oil"; Quantity = 10 }
  { Ingredient = "Lemon"; Quantity = 1 } ]

// ❌ Not OK
[{ Ingredient = "Green beans"; Quantity = 250 }
 { Ingredient = "Pine nuts"; Quantity = 250 }
 { Ingredient = "Feta cheese"; Quantity = 250 }
 { Ingredient = "Olive oil"; Quantity = 10 }
 { Ingredient = "Lemon"; Quantity = 1 }]

То же правило применяется к спискам или массивам кортежей.

Списки и массивы, разделенные по нескольким строкам, следуют аналогичному правилу, как записи:

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

Как и в случае с записями, объявление открывающих и закрывающих скобок в собственной строке упрощает перемещение кода и передачу его в функции.

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

Если выражение списка или массива находится справа от привязки, вы можете предпочесть использовать стиль 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 |] 
|]

Однако если выражение списка или массива не находится на правой стороне привязки, например, когда оно внутри другого списка или массива, если это внутреннее выражение должно охватывать несколько строк, квадратные скобки должны переходить по собственным строкам.

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

То же правило применяется к типам записей внутри массивов или списков:

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

При создании массивов и списков программным способом предпочитайте -> вместо do ... yield, когда значение всегда создается.

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

Более ранние версии F# требуют указания yield в ситуациях, когда данные могут быть созданы условно или могут быть последовательные выражения для оценки. Рекомендуется пропускать эти yield ключевые слова, если вам не нужно компилировать со старой версией 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"
    ]

В некоторых случаях do...yield может помочь в удобочитаемости. Эти случаи, хотя и субъективные, следует учитывать.

Форматирование выражений записей

Короткие записи можно записать в одной строке:

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

Записи, которые длиннее, должны использовать новые строки для меток.

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

Стили форматирования многостроковых скобок

Для записей, охватывающих несколько строк, существует три часто используемых стиля форматирования: Cramped, Alignedи Stroustrup. Стиль Cramped был стилем по умолчанию для кода F#, так как он, как правило, предпочитает стили, позволяющие компилятору легко анализировать код. Aligned и Stroustrup стили позволяют упростить переупорядочение элементов, что может облегчить процесс рефакторинга кода, однако в некоторых ситуациях может потребоваться немного более развернутый код.

  • Cramped: исторический стандарт и формат записи F# по умолчанию. Открывающие скобки идут в той же строке, что и первый элемент, закрывающая скобка на той же строке, что и последний элемент.

    let rainbow = 
        { Boss1 = "Jeffrey"
          Boss2 = "Jeffrey"
          Boss3 = "Jeffrey"
          Lackeys = [ "Zippy"; "George"; "Bungle" ] }
    
  • Aligned: скобки каждая на отдельной строке, выравнены по одному и тому же столбцу.

    let rainbow =
        {
            Boss1 = "Jeffrey"
            Boss2 = "Jeffrey"
            Boss3 = "Jeffrey"
            Lackeys = ["Zippy"; "George"; "Bungle"]
        }
    
  • Stroustrup: открывающая скобка располагается на той же строке, что и привязка, а закрывающая скобка занимает отдельную строку.

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

Те же правила стиля форматирования применяются для элементов списка и массива.

Форматирование выражений обновления копий записей

Выражение копирования и обновления записи остаётся записью, поэтому применяются аналогичные рекомендации.

Короткие выражения могут помещаться в одну строку:

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

Более длинные выражения должны переноситься на новые строки и форматироваться на основе одного из указанных выше соглашений.

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

Примечание. Если используется Stroustrup стиль для выражений копирования и обновления, необходимо отступить элементы дальше, чем скопированное имя записи:

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

Сопоставление шаблонов форматирования

| Используйте для каждого условия соответствия без отступов. Если выражение короткое, можно использовать одну строку, если каждое вложенное выражение также просто.

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

Форматирование сопоставления шаблонов должно быть согласовано с форматированием выражений. Не добавляйте пробел перед открывающей скобкой аргументов шаблона:

// ✔️ OK
match x with
| Some(y) -> y
| None -> 0

// ✔️ OK
match data with
| Success(value) -> value
| Error(msg) -> failwith msg

// ❌ Not OK, pattern formatting should match expression formatting
match x with
| Some (y) -> y
| None -> 0

Однако используйте пробелы между отдельными аргументами каррирования в шаблонах, как и в выражениях.

// ✔️ OK - space between curried arguments
match x with
| Pattern arg (a, b) -> processValues arg a b

// ❌ Not OK - missing space between curried arguments
match x with
| Pattern arg(a, b) -> processValues arg a b

Если выражение справа от стрелки сопоставления шаблонов слишком велико, перенесите его на следующую строку, сделав один отступ от match/|.

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

Аналогично большим условиям if, если выражение совпадения является многострочным или превышает пределы, установленные для одной строки по умолчанию, выражение совпадения должно использовать один отступ и новую строку. Ключевое слово match должно совпадать при инкапсуляции выражения with длинного совпадения.

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

Однако более подходящий стиль для рефакторинга длинных выражений с сопоставлением образцов заключается в использовании привязки к let или отдельной функции.

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

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

Следует избегать выравнивания стрелок сопоставления шаблонов.

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

Введенное с помощью ключевого слова function сопоставление шаблонов должно иметь отступ в один уровень от начала предыдущей строки.

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

Следует в целом избегать использования function, let или let rec в функциях, отдавая предпочтение match. Если используется, правила шаблона должны соответствовать ключевому слову 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

Форматирование выражений try/with

Сопоставление по шаблону для типа исключения должно быть отступлено на том же уровне, что и 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"

Добавьте | для каждой части предложения, за исключением случаев, когда существует только одна часть предложения.

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

Форматирование именованных аргументов

Именованные аргументы должны иметь пробелы, окружающие =:

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

При сопоставлении шаблонов с использованием дискриминированных профсоюзов именованные шаблоны форматируются аналогично, например.

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

Форматирование выражений мутаций

Выражения мутаций location <- expr обычно форматируются в одной строке. Если требуется многострочный формат, поместите выражение справа на новую строку.

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

Форматирование выражений объектов

Элементы выражения объекта должны быть выровнены с member отступом на одном уровне.

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

Вы также можете использовать 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)
}

Пустые определения типов могут быть отформатированы в одной строке:

type AnEmptyType = class end

Независимо от выбранной ширины = class end страницы всегда должно находиться в одной строке.

Форматирование выражений индекса и среза

Выражения индекса не должны содержать пробелы вокруг открывающих и закрывающих квадратных скобок.

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

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

Это также относится к более старым expr.[idx] синтаксисам.

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

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

Форматирование цитируемых выражений

Символы разделителя (<@, @>, <@@, @@>) должны размещаться на отдельных строках, если выражение является многострочным.

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

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

В однострочных выражениях символы разделителя должны размещаться в той же строке, что и сам выражение.

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

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

Форматирование цепных выражений

Если цепочка выражений (приложения-функции переплетаются с .) длинна, поместите вызов каждого приложения в следующую строку. Отступите последующие ссылки в цепочке на один уровень по сравнению с ведущей ссылкой.

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

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

Ведущая ссылка может быть составлена из нескольких ссылок, если они являются простыми идентификаторами. Например, добавление полностью квалифицированного пространства имен.

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

Последующие ссылки также должны содержать простые идентификаторы.

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

Если аргументы внутри приложения-функции не помещаются в остальную часть строки, поместите каждый аргумент в следующую строку.

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

Лямбда-аргументы внутри приложения-функции должны начинаться в той же строке, что и открытие (.

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

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

Декларации форматирования

В этом разделе рассматриваются форматы объявлений различных типов.

Добавление пустых строк между объявлениями

Разделите определения функций верхнего уровня и классов одной пустой строкой. Например:

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

Если у конструкции есть комментарии XML-документа, добавьте пустую строку перед комментарием.

// ✔️ OK

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

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

Форматирование объявлений let и членов

При форматировании let и member объявлений, как правило, правая часть привязки либо располагается в одной строке, либо (если она слишком длинная) переносится на новую строку с одним уровнем отступа.

Например, следующие примеры соответствуют следующим требованиям:

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

Это не соответствует требованиям:

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

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

Экземпляры типов записей также могут размещать квадратные скобки на собственных строках:

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

Вы также можете использовать Stroustrup стиль, открывая { в той же строке, что и имя привязки:

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

Разделите членов одной пустой строкой, задокументируйте и добавьте комментарий-документацию:

// ✔️ OK

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

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

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

Для отдельных групп связанных функций можно использовать дополнительные пустые строки (экономно). Пустые строки могут быть пропущены между набором связанных однострочных выражений (например, набором реализаций-пустышек). Используйте пустые строки в функциях экономно, чтобы указать логические разделы.

Форматирование аргументов функций и элементов

При определении функции используйте пробелы вокруг каждого аргумента.

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

Если у вас есть длинное определение функции, поместите параметры на новые строки и добавьте отступ, чтобы они соответствовали уровню отступа следующего параметра.

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

Это также относится к элементам, конструкторам и параметрам, использующим кортежи.

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

Если параметры применяют каррирование, поместите символ = вместе с любым типом возвращаемого значения на новой строке.

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

Это способ избежать слишком длинных строк (в случае, если тип возвращаемого значения может иметь длинное имя) и при добавлении параметров меньше повреждения строки.

Объявления операторов форматирования

При необходимости используйте пробелы для окружения определения оператора.

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

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

Для любого пользовательского оператора, который начинается с * и содержит более одного символа, необходимо добавить пробел в начало определения, чтобы избежать неоднозначности компилятора. Из-за этого рекомендуется просто окружить определения всех операторов одним символом пробела.

Форматирование объявлений записей

Для объявлений записей по умолчанию следует отступить { в определении типа на четыре пробела, начать список меток в той же строке и выровнять элементы, если таковые есть, с маркером {:

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

Кроме того, часто предпочтительно размещать скобки на отдельной строке, с отступом для меток на дополнительные четыре пробела:

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

Можно также поместить { в конец первой строки определения типа (Stroustrup стиль):

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

Если требуются дополнительные члены, не используйте with/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
  
// ✔️ 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

Исключением из этого правила стиля является форматирование записей в соответствии со стилем Stroustrup . В этой ситуации из-за правил with компилятора ключевое слово необходимо, если требуется реализовать интерфейс или добавить дополнительные элементы:

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

При добавлении XML-документации для полей записей предпочтителен стиль Aligned или Stroustrup, и дополнительное пространство пробелов следует добавить между членами.

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

Размещение открытого маркера в новой строке и закрывающего маркера в новой строке предпочтительнее, если вы объявляете реализации или члены интерфейса в записи:

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

Эти же правила применяются к псевдонимам анонимного типа записи.

Форматирование объявлений дискриминированных объединений

Для объявлений различаемых объединений, отступ | в определении типа на четыре пробела:

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

Если есть один короткий союз, вы можете опустить ведущий |.

// ✔️ OK
type Address = Address of string

Для более длинного или многострочного объединения сохраняйте | и помещайте каждое поле объединения в новую строку с разделителем * в конце каждой строки.

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

При добавлении комментариев документации используйте пустую строку перед каждым /// комментарием.

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

Форматирование литеральных объявлений

Литералы F#, при использовании атрибута Literal, должны располагаться так, чтобы атрибут находился на своей собственной строке, и использовать PascalCase для именования:

// ✔️ OK

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

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

Избегайте размещения атрибута в той же строке, что и значение.

Форматирование объявлений модуля

Код в локальном модуле должен иметь отступ относительно модуля, но код в модуле верхнего уровня не должен иметь отступа. Элементы пространства имен не обязаны иметь отступы.

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

Форматирование do-объявлений

В объявлениях типов, функций модулей и выражениях вычислений использование do или do! иногда требуется для операций, имеющих побочные эффекты. Если эти диапазоны охватывают несколько строк, используйте отступ и новую строку, чтобы отступы оставались согласованными с let/let!. Ниже приведен пример использования do в классе:

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

Ниже приведен пример использования do! двух пробелов отступа (так как при do! использовании четырех пробелов отступа нет случайной разницы между подходами при использовании четырех пробелов отступа):

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

Форматирование операций вычислений

При создании пользовательских операций для вычислительных выражений рекомендуется использовать именование в 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
    }

Домен, который моделиируется, должен в конечном итоге управлять соглашением об именовании. Если идиоматично использовать другое соглашение, то вместо этого следует использовать это соглашение.

Если возвращаемое значение выражения является вычислительным выражением, предпочесть поместить имя ключевого слова вычислительного выражения в собственную строку:

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

Вы также можете поместить выражение вычисления в ту же строку, что и имя привязки:

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

Независимо от вашего предпочтения, вы должны стремиться оставаться согласованными во всей базе кода. Форматировщики могут позволить вам указать этот параметр для обеспечения согласованности.

Форматирование типов и аннотации типов

В этом разделе рассматриваются типы форматирования и аннотации типов. Это включает форматирование файлов подписи с расширением .fsi .

Для типов предпочтителен синтаксис префикса для обобщений (Foo<T>) с некоторыми исключениями.

F# позволяет использовать как постфиксный стиль записи универсальных типов (например, int list), так и префиксный стиль (например, list<int>). Стиль postfix можно использовать только с одним аргументом типа. Всегда предпочитайте стиль .NET, за исключением шести конкретных типов:

  1. Для списков F# используйте форму postfix: int list а не list<int>.
  2. Для вариантов F# используйте форму постфикса: int option а не option<int>.
  3. Для параметров значения F# используйте форму постфикса: int voption а не voption<int>.
  4. Для массивов F# используйте форму постфикса: int array а не array<int> .int[]
  5. Для ссылочных ячеек используйте int ref вместо ref<int> или Ref<int>.
  6. Для последовательностей F# используйте форму постфикса: int seq, а не seq<int>.

Для всех остальных типов используйте форму префикса.

Типы функций форматирования

При определении сигнатуры функции используйте пробел вокруг символа -> :

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

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

Заметки о форматировании значений и типов аргументов

При определении значений или аргументов с заметками типа используйте пробел после символа : , но не раньше:

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

Форматирование заметок многострого типа

Если аннотация типа длинная или многострочная, поместите её на следующую строку с одним уровнем отступа.

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

Для встроенных анонимных типов записей можно также использовать Stroustrup стиль:

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

Форматирование аннотаций возвращаемого типа

В аннотациях возвращаемого типа функции или члена используйте пробел до и после символа :.

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

Форматирование типов в сигнатурах

При написании полных типов функций в сигнатурах иногда необходимо разделить аргументы по нескольким строкам. Возвращаемый тип всегда имеет отступ.

Для кортежной функции аргументы разделяются *по концам каждой строки.

Например, рассмотрим функцию со следующей реализацией:

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

В соответствующем файле подписи (.fsi расширение) функция может быть отформатирована следующим образом, если требуется многострочный формат:

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

Аналогичным образом рассмотрим каррированную функцию:

let SampleCurriedFunction arg1 arg2 arg3 arg4 = ...

В соответствующем файле сигнатуры -> помещаются в конец каждой строки.

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

Аналогичным образом рассмотрим функцию, которая принимает сочетание курриированных и кортежных аргументов:

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

В соответствующем файле сигнатуры типы, которые следуют за кортежем, имеют отступ.

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

Те же правила применяются для членов в сигнатурах типов:

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

Форматирование явных аргументов универсального типа и ограничений

Приведенные ниже рекомендации применяются к определениям функций, определениям элементов, определениям типов и приложениям-функциям.

Сохраняйте аргументы и ограничения универсального типа в одной строке, если она не слишком длинна:

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

Если аргументы или ограничения универсального типа и параметры функции не соответствуют, но параметры типа и ограничения выполняются отдельно, поместите параметры в новые строки:

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

Если параметры или ограничения типа слишком длинные, разорвите и выровняйте их, как показано ниже. Сохраняйте список параметров типа в той же строке, что и функция, независимо от его длины. Для ограничений следует поместить when в первую строку и сохранить каждое ограничение на одной строке независимо от его длины. Поместите > в конце последней строки. Увеличьте отступ ограничений на один уровень.

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

Если параметры и ограничения типа разбиты, но нет нормальных параметров функции, поместите = его в новую строку независимо от следующего:

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

Те же правила применяются к приложениям-функциям:

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

Наследование форматирования

Аргументы конструктора базового класса отображаются в списке аргументов в предложении inherit . inherit Поместите предложение на новую строку с отступом на один уровень.

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)

Если конструктор длинный или многострочный, поместите их на следующую строку с отступом на один уровень.
Отформатируйте этот многострочный конструктор в соответствии с правилами многострочных вызовов функций.

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

Форматирование основного конструктора

В соглашениях о форматировании по умолчанию пробелы не добавляются между именем типа и скобками для основного конструктора.

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

Несколько конструкторов

inherit Если предложение является частью записи, поместите его в ту же строку, если она коротка. Перенесите его на следующую строку, сделав отступ на один уровень, если она длинная или содержит несколько строк.

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 }

Атрибуты форматирования

Атрибуты помещаются над конструкцией:

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

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

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

Они должны идти после любой XML-документации:

// ✔️ OK

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

Форматирование атрибутов параметров

Атрибуты также можно поместить в параметры. В этом случае поместите в ту же строку, что и параметр, и перед именем:

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

Форматирование нескольких атрибутов

При применении нескольких атрибутов к конструкции, которая не является параметром, поместите каждый атрибут в отдельную строку:

// ✔️ OK

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

При применении к параметру поместите атрибуты в одну строку и разделите их разделителем ; .

Благодарности

Эти рекомендации основаны на комплексном руководстве по соглашениям о форматировании F# от Анха-Дунга Фана.