F# 코드 서식 지정 지침

이 문서에서는 F# 코드가 다음과 같이 되도록 코드의 형식을 지정하는 방법에 대한 지침을 제공합니다.

  • 더 읽기 쉬운
  • Visual Studio Code 및 기타 편집기에서 도구 서식을 지정하여 적용되는 규칙에 따라
  • 온라인의 다른 코드와 유사

명명 규칙도 다루는 코딩 규칙구성 요소 디자인 지침을 참조하세요.

자동 코드 서식 지정

Fantomas 코드 포맷터는 자동 코드 서식 지정을 위한 F# 커뮤니티 표준 도구입니다. 기본 설정은 이 스타일 가이드에 해당합니다.

이 코드 포맷터를 사용하는 것이 좋습니다. F# 팀 내에서 코드 서식 지정 사양은 팀 리포지토리에 검사 코드 포맷터에 대해 합의된 설정 파일과 관련하여 합의되고 명문화되어야 합니다.

서식 지정에 대한 일반 규칙

F#은 기본적으로 상당한 공백을 사용하며 공백을 구분합니다. 다음 지침은 이것이 부과할 수 있는 몇 가지 과제를 저글링하는 방법에 대한 지침을 제공하기 위한 것입니다.

탭이 아닌 공백 사용

들여쓰기해야 하는 경우 탭이 아닌 공백을 사용해야 합니다. F# 코드는 탭을 사용하지 않으며, 문자열 리터럴 또는 주석 외부에서 탭 문자가 발견되면 컴파일러에서 오류가 발생합니다.

일관된 들여쓰기 사용

들여쓰기 시 하나 이상의 공간이 필요합니다. 조직에서 코딩 표준을 만들어 들여쓰기에 사용할 공간 수를 지정할 수 있습니다. 들여쓰기가 발생하는 각 수준에서 들여쓰기 공백 2개, 3개 또는 4개가 일반적입니다.

들여쓰기당 4개의 공백을 사용하는 것이 좋습니다.

즉, 프로그램의 들여쓰기는 주관적인 문제입니다. 변형은 정상이지만 첫 번째 규칙은 들여쓰기의 일관성입니다. 일반적으로 허용되는 들여쓰기 스타일을 선택하고 코드베이스 전체에서 체계적으로 사용합니다.

이름 길이에 중요한 서식을 사용하지 마세요.

이름 지정에 중요한 들여쓰기 및 맞춤을 피하려고 합니다.

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

가독성 또는 인수 이름 목록이 너무 길기 때문에 새 줄의 함수에 인수를 전달해야 할 수 있습니다. 이 경우 한 수준을 들여쓰기합니다.

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

비슷한 방식으로 일치 람다를 처리합니다.

// ✔️ 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 subtractThenAdd x = x - 1 + 3

특정 서식 선택 항목과 결합된 경우 이진 - 연산자를 둘러싸지 못하면 단항 -으로 해석할 수 있습니다. 단항 - 연산자는 항상 부정하는 값 바로 뒤에 오도록 해야 합니다.

// ✔️ OK
let negate x = -x

// ❌ Not OK
let negateBad x = - x

연산자 뒤 - 의 공백 문자를 추가하면 다른 사용자에게 혼동이 발생할 수 있습니다.

이진 연산자를 공백으로 구분합니다. 접두사 식은 동일한 열에 대한 라인업으로 확인됩니다.

// ✔️ 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 >> 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, e1e2 짧습니다.
  • e1e2 자체가 아닙니다 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

한 줄 if/then/else 식의 규칙을 따르는 경우와 elifelse 동일한 범위 if 에서 여러 조건부가 들여쓰기됩니다.

// ✔️ 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 긴 조건 식을 캡슐화할 때 및 then 키워드(keyword) 맞춰야 합니다.

// ✔️ 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 해야 합니다. 이전 F# 언어 버전으로 컴파일해야 하는 경우가 아니면 이러한 yield 키워드(keyword) 생략하는 것이 좋습니다.

// ✔️ 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인 , AlignedStroustrup. Cramped 이 스타일은 컴파일러가 코드를 쉽게 구문 분석할 수 있도록 하는 스타일을 선호하는 경향이 있으므로 F# 코드의 기본 스타일입니다. 스타일과 Stroustrup 스타일을 모두 Aligned 사용하면 멤버의 순서를 더 쉽게 다시 정렬할 수 있으므로 리팩터링이 더 쉬울 수 있는 코드가 생성되며, 특정 상황에서는 약간 더 자세한 코드가 필요할 수 있다는 단점이 있습니다.

  • 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" 
}

서식 패턴 일치

들여쓰기 없이 일치 항목의 각 절에 대해 a | 를 사용합니다. 식이 짧으면 각 하위 식도 간단한 경우 한 줄 사용을 고려할 수 있습니다.

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

패턴 일치 화살표의 오른쪽에 있는 식이 너무 크면 다음 줄로 이동하여 한 단계씩 match/|들여쓰기합니다.

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

큰 경우 조건과 마찬가지로 일치 식이 여러 줄이거나 한 줄의 기본 허용 오차를 초과하는 경우 일치 식은 들여쓰기 하나와 새 줄을 사용해야 합니다. match 긴 일치 식을 캡슐화할 때 및 with 키워드(keyword) 맞춰야 합니다.

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

키워드(keyword) function 사용하여 도입된 패턴 일치는 이전 줄의 시작부터 한 수준까지 들여쓰기해야 합니다.

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

에 의해 let 정의되거나 let rec 일반적으로 정의되는 함수의 function 사용은 을 위해 피match해야 합니다. 사용하는 경우 패턴 규칙은 키워드(keyword) 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"

| 단일 절만 있는 경우를 제외하고 각 절에 대해 a를 추가합니다.

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

또한 4개의 공백으로 들여쓰기된 레이블을 사용하여 자체 줄에 대괄호를 배치하는 것이 일반적입니다.

// ✔️ 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 으로 인해 인터페이스를 구현하거나 멤버를 추가하려는 경우 키워드(keyword) 필요합니다.

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

레코드 필드에 Aligned 대한 XML 설명서가 추가되거나 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

익명 레코드 형식 별칭에 대해 동일한 규칙이 적용됩니다.

구분된 공용 구조체 선언 서식 지정

구분된 공용 구조체 선언의 경우 형식 정의를 4개의 공백으로 들여쓰 | 기합니다.

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

리터럴 선언 서식 지정

특성을 사용하는 Literal F# 리터럴은 특성을 자체 줄에 배치하고 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! 입니다(들여쓰기 공백 4개를 사용할 때 공교롭게도 접근 방식 간에 차이가 없기 때문 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
    }

모델링되는 do기본 궁극적으로 명명 규칙을 추진해야 합니다. 다른 규칙을 사용하는 것이 idiomatic인 경우 해당 규칙을 대신 사용해야 합니다.

식의 반환 값이 계산 식인 경우 계산 식을 고유한 줄에 키워드(keyword) 이름을 배치하는 것이 좋습니다.

// ✔️ 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>)을 모두 사용할 수 있습니다. 후위 스타일은 단일 형식 인수와 함께만 사용할 수 있습니다. 5가지 특정 형식을 제외하고 항상 .NET 스타일을 선호합니다.

  1. F# 목록의 경우 . int listlist<int>
  2. F# 옵션의 경우 . int optionoption<int>
  3. F# 값 옵션의 경우 . int voptionvoption<int>
  4. F# 배열의 경우 접두사 형식 int array 을 사용하십시오 array<int>int[].
  5. 참조 셀의 경우 대신 ref<int> 사용 int ref 하거나 Ref<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 }

매개 변수에 적용된 경우 동일한 줄에 특성을 배치하고 구분 기호로 ; 구분합니다.

승인

이러한 지침은 Anh-Dung Phan의 F# 서식 규칙에 대한 포괄적인 가이드를 기반으로 합니다.