F# 구성 요소 디자인 지침

이 문서는 F# 구성 요소 디자인 지침, v14, Microsoft Research 및 F# Software Foundation에서 원래 큐레이팅 및 유지 관리된 버전을 기반으로 하는 F# 프로그래밍에 대한 구성 요소 디자인 지침 집합입니다.

이 문서에서는 사용자가 F# 프로그래밍에 익숙하다고 가정합니다. 이 가이드의 다양한 버전에 대한 기여와 유용한 피드백에 대해 F# 커뮤니티에 감사드립니다.

개요

이 문서에서는 F# 구성 요소 디자인 및 코딩과 관련된 몇 가지 문제를 살펴봅합니다. 구성 요소는 다음 중 어느 것을 의미할 수 있습니다.

  • 해당 프로젝트 내에 외부 소비자가 있는 F# 프로젝트의 계층입니다.
  • 어셈블리 경계를 넘어 F# 코드에서 사용할 수 있는 라이브러리입니다.
  • 어셈블리 경계를 넘어 .NET 언어에서 사용할 수 있는 라이브러리입니다.
  • NuGet 같은 패키지 리포지토리를 통해 배포하기 위한 라이브러리입니다.

이 문서에서 설명하는 기술은 좋은 F# 코드의 5가지 원칙을 따르므로 기능 및 개체 프로그래밍을 적절하게 활용합니다.

방법론에 관계없이 구성 요소 및 라이브러리 디자이너는 개발자가 가장 쉽게 사용할 수 있는 API를 만들 때 많은 실용적이고 전문적인 문제에 직면합니다. .NET 라이브러리 디자인 지침의 양심적 애플리케이션은 사용이 편리한 일관된 API 집합을 만드는 방향으로 안내합니다.

일반 지침

라이브러리에 대한 대상 그룹에 관계없이 F# 라이브러리에 적용되는 몇 가지 범용 지침이 있습니다.

.NET 라이브러리 디자인 지침 알아보기

수행하는 F# 코딩의 종류에 관계없이 .NET 라이브러리 디자인 지침에 대한 실무 지식을 갖출 수 있습니다. 대부분의 다른 F# 및 .NET 프로그래머는 이러한 지침에 익숙하며 .NET 코드가 이를 준수할 것으로 예상합니다.

.NET 라이브러리 디자인 지침은 명명, 클래스 및 인터페이스 디자인, 멤버 디자인(속성, 메서드, 이벤트 등) 등에 대한 일반적인 지침을 제공하며 다양한 디자인 지침에 대한 유용한 첫 번째 참조 지점입니다.

코드에 XML 설명서 주석 추가

공용 API에 대한 XML 설명서를 통해 사용자는 이러한 형식과 멤버를 사용할 때 훌륭한 Intellisense 및 Quickinfo를 얻고 라이브러리에 대한 문서 파일을 빌드할 수 있습니다. xmldoc 주석 내의 추가 태그에 사용할 수 있는 다양한 xml 태그에 대한 XML 설명서를 참조하세요.

/// A class for representing (x,y) coordinates
type Point =

    /// Computes the distance between this point and another
    member DistanceTo: otherPoint:Point -> float

짧은 형식의 XML 주석() 또는 표준 XML 주석(/// comment///<summary>comment</summary>)을 사용할 수 있습니다.

안정적인 라이브러리 및 구성 요소 API에 명시적 서명 파일(.fsi)을 사용하는 것이 좋습니다.

F# 라이브러리에서 명시적 서명 파일을 사용하면 공용 API에 대한 간결한 요약을 제공하므로 라이브러리의 전체 공개 화면을 알 수 있도록 하고 공용 설명서와 내부 구현 세부 정보 간에 명확한 구분을 제공합니다. 서명 파일은 구현 파일과 서명 파일 모두에서 변경해야 하므로 공용 API 변경에 마찰을 더합니다. 따라서 서명 파일은 일반적으로 API가 굳어지고 더 이상 크게 변경되지 않을 때만 도입되어야 합니다.

항상 .NET에서 문자열을 사용하는 모범 사례를 따릅니다.

.NET 지침에서 문자열 사용에 대한 모범 사례를 따릅니다. 특히 항상 문자열 변환 및 비교에서 문화권 의도 를 명시적으로 명시합니다(해당하는 경우).

F#연결 라이브러리에 대한 지침

이 섹션에서는 공용 F#연결 라이브러리를 개발하기 위한 권장 사항을 제공합니다. 즉, F# 개발자가 사용할 공용 API를 노출하는 라이브러리입니다. 특히 F#에 적용할 수 있는 다양한 라이브러리 디자인 권장 사항이 있습니다. 다음과 같은 특정 권장 사항이 없는 경우 .NET 라이브러리 디자인 지침은 대체 지침입니다.

명명 규칙

.NET 명명 및 대문자 지정 규칙 사용

다음 표에서는 .NET 명명 및 대문자 지정 규칙을 따릅니다. F# 구문도 포함할 수 있는 작은 추가 사항이 있습니다.

구문 사례 부분 참고
구체적인 형식 PascalCase 명사/형용사 목록, 이중, 복합 구체적인 형식은 구조체, 클래스, 열거형, 대리자, 레코드 및 공용 구조체입니다. 형식 이름은 일반적으로 OCaml에서 소문자이지만 F#은 형식에 대한 .NET 명명 체계를 채택했습니다.
DLL PascalCase Fabrikam.Core.dll
공용 구조체 태그 PascalCase 명사 일부, 추가, 성공 공용 API에는 접두사 사용 안 함 필요에 따라 다음과 같이 내부일 때 접두사를 사용합니다. type Teams = TAlpha | TBeta | TDelta.
이벤트 PascalCase 동사 ValueChanged / ValueChanging
예외 PascalCase WebException 이름은 "예외"로 끝나야 합니다.
필드 PascalCase 명사 CurrentName
인터페이스 형식 PascalCase 명사/형용사 IDisposable 이름은 "I"로 시작해야 합니다.
메서드 PascalCase 동사 ToString
네임스페이스 PascalCase Microsoft.FSharp.Core 일반적으로 기술이 조직과 독립적인 경우 조직을 삭제하지만 사용합니다 <Organization>.<Technology>[.<Subnamespace>].
매개 변수 camelCase 명사 typeName, transform, range
let 값(내부) camelCase 또는 PascalCase 명사/동사 getValue, myTable
let 값(외부) camelCase 또는 PascalCase 명사/동사 List.map, Dates.Today 렛바운드 값은 기존의 기능 디자인 패턴을 따르는 경우 공용인 경우가 많습니다. 그러나 일반적으로 다른 .NET 언어에서 식별자를 사용할 수 있는 경우 PascalCase를 사용합니다.
속성 PascalCase 명사/형용사 IsEndOfFile, BackColor 부울 속성은 일반적으로 Is 및 Can을 사용하며 IsNotEndOfFile이 아닌 IsEndOfFile에서와 같이 긍정되어야 합니다.

약어 방지

.NET 지침은 약어 사용을 권장하지 않습니다(예: "대신 OnBtnClick사용OnButtonClick). "비동기"와 같은 Async 일반적인 약어는 허용됩니다. 이 지침은 기능 프로그래밍에 대해 무시되는 경우가 있습니다. 예를 들어 List.iter "반복"에 약어를 사용합니다. 이러한 이유로 약어를 사용하는 것은 F#-F# 프로그래밍에서 더 큰 수준까지 허용되는 경향이 있지만 공용 구성 요소 디자인에서는 일반적으로 피해야 합니다.

대/소문자 이름 충돌 방지

.NET 지침에 따르면 일부 클라이언트 언어(예: Visual Basic)는 대/소문자를 구분하지 않으므로 대/소문자만 사용하여 이름 충돌을 구분할 수 없습니다.

적절한 경우 머리글자어 사용

XML과 같은 약어는 약어가 아니며 .NET 라이브러리에서 캡슐화되지 않은 형식(Xml)으로 널리 사용됩니다. 잘 알려진 널리 알려진 약어만 사용해야 합니다.

제네릭 매개 변수 이름에 PascalCase 사용

F#연결 라이브러리를 포함하여 공용 API의 제네릭 매개 변수 이름에 PascalCase를 사용합니다. 특히 임의 제네릭 매개 변수에 대해 , U, , T1T2 등의 T이름을 사용하고 특정 이름이 적합한 경우 F#을 향한 라이브러리의 경우 , ValueArg (예TKey: 그렇지 않음)와 같은 Key이름을 사용합니다.

F# 모듈의 공용 함수 및 값에 PascalCase 또는 camelCase 사용

camelCase는 정규화되지 않은(예: ) 및 "표준 컬렉션 함수"(예 invalidArg: List.map)에 사용하도록 설계된 공용 함수에 사용됩니다. 두 경우 모두 함수 이름은 언어의 키워드와 매우 유사합니다.

개체, 형식 및 모듈 디자인

네임스페이스 또는 모듈을 사용하여 형식 및 모듈 포함

구성 요소의 각 F# 파일은 네임스페이스 선언 또는 모듈 선언으로 시작해야 합니다.

namespace Fabrikam.BasicOperationsAndTypes

type ObjectType1() =
    ...

type ObjectType2() =
     ...

module CommonOperations =
    ...

또는

module Fabrikam.BasicOperationsAndTypes

type ObjectType1() =
    ...

type ObjectType2() =
    ...

module CommonOperations =
    ...

모듈과 네임스페이스를 사용하여 최상위 수준에서 코드를 구성하는 경우의 차이점은 다음과 같습니다.

  • 네임스페이스는 여러 파일에 걸쳐 있습니다.
  • 네임스페이스는 내부 모듈 내에 있지 않으면 F# 함수를 포함할 수 없습니다.
  • 지정된 모듈에 대한 코드는 단일 파일 내에 포함되어야 합니다.
  • 최상위 모듈은 내부 모듈 없이도 F# 함수를 포함할 수 있습니다.

최상위 네임스페이스 또는 모듈 중에서 선택하는 것은 컴파일된 코드 형식에 영향을 줍니다. 따라서 API가 결국 F# 코드 외부에서 사용될 경우 다른 .NET 언어의 보기에 영향을 줍니다.

개체 형식에 내장된 작업에 메서드 및 속성 사용

개체를 사용할 때 소모성 기능이 해당 형식의 메서드 및 속성으로 구현되도록 하는 것이 가장 좋습니다.

type HardwareDevice() =

    member this.ID = ...

    member this.SupportedProtocols = ...

type HashTable<'Key,'Value>(comparer: IEqualityComparer<'Key>) =

    member this.Add(key, value) = ...

    member this.ContainsKey(key) = ...

    member this.ContainsValue(value) = ...

지정된 멤버에 대한 많은 기능을 해당 멤버에서 반드시 구현할 필요는 없지만 해당 기능의 소모성 부분이 있어야 합니다.

클래스를 사용하여 변경 가능한 상태 캡슐화

F#에서는 종료, 시퀀스 식 또는 비동기 계산과 같은 다른 언어 구문에 의해 해당 상태가 아직 캡슐화되지 않은 경우에만 이 작업을 수행해야 합니다.

type Counter() =
    // let-bound values are private in classes.
    let mutable count = 0

    member this.Next() =
        count <- count + 1
        count

인터페이스 형식을 사용하여 작업 집합을 나타냅니다. 함수의 튜플 또는 함수 레코드와 같은 다른 옵션에 선호됩니다.

type Serializer =
    abstract Serialize<'T> : preserveRefEq: bool -> value: 'T -> string
    abstract Deserialize<'T> : preserveRefEq: bool -> pickle: string -> 'T

기본 설정:

type Serializer<'T> = {
    Serialize: bool -> 'T -> string
    Deserialize: bool -> string -> 'T
}

인터페이스는 일반적으로 Functors가 제공하는 것을 달성하는 데 사용할 수 있는 .NET의 일류 개념입니다. 또한 실존적 형식을 프로그램에 인코딩하는 데 사용할 수 있으며, 함수 레코드는 인코딩할 수 없습니다.

모듈을 사용하여 컬렉션에서 작동하는 함수 그룹화

컬렉션 형식을 정의할 때 새 컬렉션 형식에 대한 표준 작업 집합(예: CollectionType.mapCollectionType.iter)을 제공하는 것이 좋습니다.

module CollectionType =
    let map f c =
        ...
    let iter f c =
        ...

이러한 모듈을 포함하는 경우 FSharp.Core에 있는 함수에 대한 표준 명명 규칙을 따릅니다.

모듈을 사용하여 일반적인 정식 함수, 특히 수학 및 DSL 라이브러리의 함수를 그룹화합니다.

예를 들어 FSharp.Core.dll Microsoft.FSharp.Core.Operators 제공하는 최상위 함수(예: abssin)의 자동으로 열린 컬렉션입니다.

마찬가지로 통계 라이브러리에는 함수 erf 가 있는 모듈과 erfc이 모듈이 명시적으로 또는 자동으로 열리도록 디자인된 모듈이 포함될 수 있습니다.

RequireQualifiedAccess를 사용하고 AutoOpen 특성을 신중하게 적용하는 것이 좋습니다.

모듈에 [<RequireQualifiedAccess>] 특성을 추가하면 모듈이 열리지 않을 수 있으며 모듈의 요소에 대한 참조에 명시적 정규화된 액세스가 필요하다는 것을 나타냅니다. 예를 들어 모듈에는 Microsoft.FSharp.Collections.List 이 특성이 있습니다.

이 기능은 모듈의 함수 및 값에 다른 모듈의 이름과 충돌할 가능성이 있는 이름이 있는 경우에 유용합니다. 정규화된 액세스가 필요하면 라이브러리의 장기적인 유지 관리 효율성과 진화성을 크게 높일 수 있습니다.

모듈에 [<AutoOpen>] 특성을 추가하면 포함된 네임스페이스가 열릴 때 모듈이 열립니다. [<AutoOpen>] 어셈블리를 참조할 때 자동으로 열리는 모듈을 나타내기 위해 어셈블리에 특성을 적용할 수도 있습니다.

예를 들어 통계 라이브러리 MathsHeaven.Statistics 에는 module MathsHeaven.Statistics.Operators 포함하는 함수 erferfc. 이 모듈을 .로 [<AutoOpen>]표시하는 것이 좋습니다. 즉 open MathsHeaven.Statistics , 이 모듈을 열고 이름을 erferfc 범위로 가져옵니다. 또 다른 용도 [<AutoOpen>] 는 확장 메서드를 포함하는 모듈에 대한 것입니다.

잠재 고객을 오염된 네임스페이스로 과도하게 사용하고 [<AutoOpen>] 특성을 주의하여 사용해야 합니다. 특정 도메인의 특정 라이브러리의 경우 신중하게 사용하면 [<AutoOpen>] 유용성이 향상됩니다.

잘 알려진 연산자를 사용하는 것이 적절한 클래스에서 연산자 멤버를 정의하는 것이 좋습니다.

경우에 따라 클래스는 Vectors와 같은 수학 구문을 모델링하는 데 사용됩니다. 모델링되는 도메인에 잘 알려진 연산자가 있는 경우 클래스에 내장된 멤버로 정의하는 것이 유용합니다.

type Vector(x: float) =

    member v.X = x

    static member (*) (vector: Vector, scalar: float) = Vector(vector.X * scalar)

    static member (+) (vector1: Vector, vector2: Vector) = Vector(vector1.X + vector2.X)

let v = Vector(5.0)

let u = v * 10.0

이 지침은 이러한 형식에 대한 일반적인 .NET 지침에 해당합니다. 그러나 F# 코딩에서는 이러한 형식을 F# 함수 및 List.sumBy와 같은 멤버 제약 조건이 있는 메서드와 함께 사용할 수 있으므로 추가로 중요할 수 있습니다.

CompiledName을 사용하여 .를 제공하는 것이 좋습니다. 다른 .NET 언어 소비자에 대한 NET 친화적인 이름

경우에 따라 F# 소비자에 대해 한 가지 스타일(예: 모듈 바인딩 함수인 것처럼 표시되도록 소문자의 정적 멤버)의 이름을 지정하려고 하지만 어셈블리로 컴파일될 때 이름에 다른 스타일을 지정할 수 있습니다. 이 특성을 사용하여 [<CompiledName>] 어셈블리를 사용하는 F# 코드가 아닌 다른 스타일을 제공할 수 있습니다.

type Vector(x:float, y:float) =

    member v.X = x
    member v.Y = y

    [<CompiledName("Create")>]
    static member create x y = Vector (x, y)

let v = Vector.create 5.0 3.0

사용 하 여 [<CompiledName>]어셈블리의 비 F# 소비자에 대 한 .NET 명명 규칙을 사용할 수 있습니다.

멤버 함수에 대한 메서드 오버로드를 사용합니다. 이 경우 더 간단한 API를 제공합니다.

메서드 오버로딩은 비슷한 기능을 수행해야 하지만 다른 옵션이나 인수를 사용하여 API를 간소화하는 강력한 도구입니다.

type Logger() =

    member this.Log(message) =
        ...
    member this.Log(message, retryPolicy) =
        ...

F#에서는 인수 형식보다는 인수 수에 오버로드하는 것이 더 일반적입니다.

이러한 형식의 디자인이 발전할 가능성이 있는 경우 레코드 및 공용 구조체 형식의 표현 숨기기

개체의 구체적인 표현을 표시하지 않습니다. 예를 들어 값의 DateTime 구체적인 표현은 .NET 라이브러리 디자인의 외부 공용 API에 의해 표시되지 않습니다. 런타임에 공용 언어 런타임은 실행 전체에서 사용될 커밋된 구현을 알고 있습니다. 그러나 컴파일된 코드 자체는 구체적인 표현에 대한 종속성을 선택하지 않습니다.

확장성을 위해 구현 상속을 사용하지 마십시오.

F#에서는 구현 상속이 거의 사용되지 않습니다. 또한 상속 계층 구조는 종종 복잡하고 새로운 요구 사항이 도착하면 변경하기 어렵습니다. F#에서는 호환성을 위해 상속 구현이 여전히 존재하며 문제가 가장 적합한 경우는 드물지만 인터페이스 구현과 같은 다형성을 디자인할 때는 F# 프로그램에서 대체 기법을 찾아야 합니다.

함수 및 멤버 서명

적은 수의 관련 없는 여러 값을 반환할 때 반환 값에 튜플 사용

다음은 반환 형식에서 튜플을 사용하는 좋은 예입니다.

val divrem: BigInteger -> BigInteger -> BigInteger * BigInteger

많은 구성 요소를 포함하는 반환 형식 또는 구성 요소가 식별 가능한 단일 엔터티와 관련된 경우 튜플 대신 명명된 형식을 사용하는 것이 좋습니다.

F# API 경계에서 비동기 프로그래밍에 사용 Async<T>

명명된 동기 연산이 T반환되는 경우 비동기 연산의 이름을 반환하거나 OperationAsync 반환 Task<T>Async<T> 하는 경우 이름을 Operation 지정 AsyncOperation 해야 합니다. Begin/End 메서드를 노출하는 일반적으로 사용되는 .NET 형식의 경우 확장 메서드 Async.FromBeginEnd 를 외관으로 작성하여 해당 .NET API에 F# 비동기 프로그래밍 모델을 제공하는 것이 좋습니다.

type SomeType =
    member this.Compute(x:int): int =
        ...
    member this.AsyncCompute(x:int): Async<int> =
        ...

type System.ServiceModel.Channels.IInputChannel with
    member this.AsyncReceive() =
        ...

예외

예외, 결과 및 옵션의 적절한 사용에 대해 알아보려면 오류 관리를 참조하세요.

확장 멤버

F#-to-F# 구성 요소에 F# 확장 멤버를 신중하게 적용

F# 확장 멤버는 일반적으로 대부분의 사용 모드에서 형식과 연결된 내장 연산을 닫는 작업에만 사용해야 합니다. 한 가지 일반적인 용도는 다양한 .NET 형식에 대해 F#에 보다 관용적인 API를 제공하는 것입니다.

type System.ServiceModel.Channels.IInputChannel with
    member this.AsyncReceive() =
        Async.FromBeginEnd(this.BeginReceive, this.EndReceive)

type System.Collections.Generic.IDictionary<'Key,'Value> with
    member this.TryGet key =
        let ok, v = this.TryGetValue key
        if ok then Some v else None

공용 구조체 형식

트리 구조화된 데이터에 클래스 계층 구조 대신 구분된 공용 구조체 사용

트리와 유사한 구조는 재귀적으로 정의됩니다. 이것은 상속에 어색하지만 차별적 인 연합으로 우아합니다.

type BST<'T> =
    | Empty
    | Node of 'T * BST<'T> * BST<'T>

또한 구분된 공용 구조체를 사용하여 트리와 유사한 데이터를 표시하면 패턴 일치의 완전성을 활용할 수 있습니다.

대/소문자 이름이 충분히 고유하지 않은 공용 구조체 형식에서 사용 [<RequireQualifiedAccess>]

동일한 이름이 차별된 공용 구조체 사례와 같은 다양한 항목에 가장 적합한 도메인에서 자신을 찾을 수 있습니다. 문 순서에 따라 섀도링으로 인해 혼란스러운 오류가 트리거되는 것을 방지하기 위해 대/소문자 이름을 명확하게 구분하는 open 데 사용할 [<RequireQualifiedAccess>] 수 있습니다.

이러한 형식의 디자인이 진화할 가능성이 있는 경우 이진 호환 API에 대해 구분된 공용 구조체의 표현 숨기기

공용 구조체 형식은 간결한 프로그래밍 모델에 F# 패턴 일치 형식을 사용합니다. 앞에서 설명한 것처럼 이러한 형식의 디자인이 진화할 가능성이 있는 경우 구체적인 데이터 표현을 표시하지 않아야 합니다.

예를 들어 비공개 또는 내부 선언을 사용하거나 서명 파일을 사용하여 구분된 공용 구조체의 표현을 숨길 수 있습니다.

type Union =
    private
    | CaseA of int
    | CaseB of string

차별된 공용 구조체를 무차별적으로 공개하는 경우 사용자 코드를 위반하지 않고 라이브러리의 버전을 지정하기가 어려울 수 있습니다. 대신 하나 이상의 활성 패턴을 표시하여 형식 값에 대한 패턴 일치를 허용하는 것이 좋습니다.

활성 패턴은 F# 공용 구조체 형식을 직접 노출하지 않도록 하면서 F# 소비자에게 패턴 일치를 제공하는 대체 방법을 제공합니다.

인라인 함수 및 멤버 제약 조건

묵시적 멤버 제약 조건 및 정적으로 확인된 제네릭 형식이 있는 인라인 함수를 사용하여 제네릭 숫자 알고리즘 정의

산술 멤버 제약 조건 및 F# 비교 제약 조건은 F# 프로그래밍의 표준입니다. 예를 들어, 다음 코드를 고려하세요.

let inline highestCommonFactor a b =
    let rec loop a b =
        if a = LanguagePrimitives.GenericZero<_> then b
        elif a < b then loop a (b - a)
        else loop (a - b) b
    loop a b

이 함수의 형식은 다음과 같습니다.

val inline highestCommonFactor : ^T -> ^T -> ^T
                when ^T : (static member Zero : ^T)
                and ^T : (static member ( - ) : ^T * ^T -> ^T)
                and ^T : equality
                and ^T : comparison

이는 수학 라이브러리의 공용 API에 적합한 함수입니다.

멤버 제약 조건을 사용하여 형식 클래스 및 오리 입력 시뮬레이션 방지

F# 멤버 제약 조건을 사용하여 "오리 입력"을 시뮬레이션할 수 있습니다. 그러나 이를 사용하는 멤버는 일반적으로 F#-to-F# 라이브러리 디자인에서 사용해서는 안 됩니다. 이는 익숙하지 않거나 비표준 암시적 제약 조건을 기반으로 하는 라이브러리 디자인으로 인해 사용자 코드가 유연하지 않고 하나의 특정 프레임워크 패턴에 연결되는 경향이 있기 때문입니다.

또한 이러한 방식으로 멤버 제약 조건을 많이 사용하면 컴파일 시간이 매우 길어질 수 있습니다.

연산자 정의

사용자 지정 기호 연산자를 정의하지 않습니다.

사용자 지정 연산자는 일부 상황에서 필수적이며 구현 코드의 큰 본문 내에서 매우 유용한 표기법 디바이스입니다. 라이브러리의 새 사용자의 경우 명명된 함수를 사용하는 것이 더 쉬운 경우가 많습니다. 또한 사용자 지정 기호 연산자는 문서화하기 어려울 수 있으며 사용자는 IDE 및 검색 엔진의 기존 제한 사항으로 인해 연산자에서 도움말을 찾기가 더 어렵습니다.

따라서 기능을 명명된 함수 및 멤버로 게시하고, 표기법상의 이점이 설명서 및 인식 비용보다 큰 경우에만 이 기능에 대한 연산자를 추가로 노출하는 것이 가장 좋습니다.

측정 단위

F# 코드에서 형식 안전성을 강화하기 위해 측정 단위를 신중하게 사용합니다.

측정 단위에 대한 추가 입력 정보는 다른 .NET 언어로 볼 때 지워집니다. .NET 구성 요소, 도구 및 리플렉션에는 types-sans-units가 표시됩니다. 예를 들어 C# 소비자는 floatfloat<kg>.

형식 약어

형식 약어를 신중하게 사용하여 F# 코드 간소화

.NET 구성 요소, 도구 및 리플렉션에는 형식에 대한 축약된 이름이 표시되지 않습니다. 형식 약어를 많이 사용하면 도메인이 실제로보다 더 복잡하게 표시되어 소비자를 혼동할 수 있습니다.

멤버 및 속성이 축약되는 형식에서 사용할 수 있는 형식과 본질적으로 달라야 하는 공용 형식의 경우 형식 약어를 사용하지 않도록 합니다.

이 경우 축약되는 형식은 정의되는 실제 형식의 표현에 대해 너무 많이 표시됩니다. 대신 약어를 클래스 형식 또는 단일 대/소문자 구분 공용 구조체로 래핑하는 것이 좋습니다(또는 성능이 필수적인 경우 구조체 형식을 사용하여 약어를 래핑하는 것이 좋습니다).

예를 들어 다중 맵을 F# 맵의 특수 사례로 정의하려는 경우가 있습니다. 예를 들면 다음과 같습니다.

type MultiMap<'Key,'Value> = Map<'Key,'Value list>

그러나 이 형식의 논리적 점 표기법 작업은 맵의 작업과 동일하지 않습니다. 예를 들어 키가 사전에 없는 경우 조회 연산자가 map[key] 예외를 발생시키는 대신 빈 목록을 반환하는 것이 좋습니다.

다른 .NET 언어에서 사용하기 위한 라이브러리에 대한 지침

다른 .NET 언어에서 사용할 라이브러리를 디자인할 때는 .NET 라이브러리 디자인 지침을 준수하는 것이 중요합니다. 이 문서에서 이러한 라이브러리는 F# 구문을 제한 없이 사용하는 F#지향 라이브러리와 달리 바닐라 .NET 라이브러리로 레이블이 지정됩니다. vanilla .NET 라이브러리를 디자인한다는 것은 공용 API에서 F#관련 구문의 사용을 최소화하여 .NET Framework 나머지 부분과 일치하는 친숙하고 관용적인 API를 제공하는 것을 의미합니다. 규칙은 다음 섹션에 설명되어 있습니다.

네임스페이스 및 형식 디자인(다른 .NET 언어에서 사용할 라이브러리용)

구성 요소의 공용 API에 .NET 명명 규칙 적용

약식 이름 및 .NET 대문자 표시 지침의 사용에 특히 주의하세요.

type pCoord = ...
    member this.theta = ...

type PolarCoordinate = ...
    member this.Theta = ...

네임스페이스, 형식 및 멤버를 구성 요소의 기본 조직 구조로 사용

공용 기능을 포함하는 모든 파일은 선언으로 namespace 시작해야 하며 네임스페이스의 유일한 공용 엔터티는 형식이어야 합니다. F# 모듈을 사용하지 마세요.

공용이 아닌 모듈을 사용하여 구현 코드, 유틸리티 형식 및 유틸리티 함수를 보유합니다.

정적 형식은 나중에 API가 오버로드 및 F# 모듈 내에서 사용되지 않을 수 있는 기타 .NET API 디자인 개념을 사용할 수 있도록 모듈보다 선호되어야 합니다.

예를 들어 다음 공용 API 대신 다음을 수행합니다.

module Fabrikam

module Utilities =
    let Name = "Bob"
    let Add2 x y = x + y
    let Add3 x y z = x + y + z

대신 다음을 고려합니다.

namespace Fabrikam

[<AbstractClass; Sealed>]
type Utilities =
    static member Name = "Bob"
    static member Add(x,y) = x + y
    static member Add(x,y,z) = x + y + z

형식 디자인이 개선되지 않는 경우 vanilla .NET API에서 F# 레코드 형식 사용

F# 레코드 형식은 간단한 .NET 클래스로 컴파일됩니다. API의 일부 간단하고 안정적인 형식에 적합합니다. 인터페이스의 자동 생성을 [<NoEquality>] 표시하지 않는 데 및 [<NoComparison>] 특성을 사용하는 것이 좋습니다. 또한 공용 필드를 노출하기 때문에 vanilla .NET API에서 변경 가능한 레코드 필드를 사용하지 않도록 합니다. 클래스가 API의 향후 진화를 위해 보다 유연한 옵션을 제공할지 항상 고려합니다.

예를 들어 다음 F# 코드는 공용 API를 C# 소비자에게 노출합니다.

F#:

[<NoEquality; NoComparison>]
type MyRecord =
    { FirstThing: int
        SecondThing: string }

C#:

public sealed class MyRecord
{
    public MyRecord(int firstThing, string secondThing);
    public int FirstThing { get; }
    public string SecondThing { get; }
}

vanilla .NET API에서 F# 공용 구조체 형식의 표현 숨기기

F# 공용 구조체 형식은 F#-to-F# 코딩에도 구성 요소 경계에서 일반적으로 사용되지 않습니다. 구성 요소 및 라이브러리 내에서 내부적으로 사용할 때 뛰어난 구현 디바이스입니다.

vanilla .NET API를 디자인할 때 프라이빗 선언 또는 서명 파일을 사용하여 공용 구조체 형식의 표현을 숨기는 것이 좋습니다.

type PropLogic =
    private
    | And of PropLogic * PropLogic
    | Not of PropLogic
    | True

멤버와 함께 내부적으로 공용 구조체 표현을 사용하여 원하는 형식을 제공하는 형식을 보강할 수도 있습니다. NET 연결 API.

type PropLogic =
    private
    | And of PropLogic * PropLogic
    | Not of PropLogic
    | True

    /// A public member for use from C#
    member x.Evaluate =
        match x with
        | And(a,b) -> a.Evaluate && b.Evaluate
        | Not a -> not a.Evaluate
        | True -> true

    /// A public member for use from C#
    static member CreateAnd(a,b) = And(a,b)

프레임워크의 디자인 패턴을 사용하여 GUI 및 기타 구성 요소 디자인

.NET에는 WinForms, WPF 및 ASP.NET 같은 다양한 프레임워크를 사용할 수 있습니다. 이러한 프레임워크에서 사용할 구성 요소를 디자인하는 경우 각각에 대한 명명 및 디자인 규칙을 사용해야 합니다. 예를 들어 WPF 프로그래밍의 경우 디자인하는 클래스에 WPF 디자인 패턴을 채택합니다. 사용자 인터페이스 프로그래밍의 모델의 경우 이벤트 및 알림 기반 컬렉션 System.Collections.ObjectModel과 같은 디자인 패턴을 사용합니다.

개체 및 멤버 디자인(다른 .NET 언어에서 사용할 라이브러리용)

CLIEvent 특성을 사용하여 .NET 이벤트 노출

DelegateEvent 이벤트가 다른 .NET 언어에 친숙한 방식으로 게시되도록 개체를 EventArgs 사용하는 특정 .NET 대리자 형식을 사용하여 생성합니다(기본적으로 형식을 사용하는 FSharpHandler 것이 아니라Event).

type MyBadType() =
    let myEv = new Event<int>()

    [<CLIEvent>]
    member this.MyEvent = myEv.Publish

type MyEventArgs(x: int) =
    inherit System.EventArgs()
    member this.X = x

    /// A type in a component designed for use from other .NET languages
type MyGoodType() =
    let myEv = new DelegateEvent<EventHandler<MyEventArgs>>()

    [<CLIEvent>]
    member this.MyEvent = myEv.Publish

.NET 작업을 반환하는 메서드로 비동기 작업 노출

작업은 .NET에서 활성 비동기 계산을 나타내는 데 사용됩니다. 태스크는 "이미 실행 중인" 작업을 나타내고 병렬 컴퍼지션을 수행하거나 취소 신호 및 기타 상황별 매개 변수의 전파를 숨기는 방식으로 함께 구성할 수 없으므로 일반적으로 F# Async<T> 개체보다 덜 구성됩니다.

그러나 그럼에도 불구하고 작업을 반환하는 메서드는 .NET에서 비동기 프로그래밍의 표준 표현입니다.

/// A type in a component designed for use from other .NET languages
type MyType() =

    let compute (x: int): Async<int> = async { ... }

    member this.ComputeAsync(x) = compute x |> Async.StartAsTask

명시적 취소 토큰도 자주 수락하려고 합니다.

/// A type in a component designed for use from other .NET languages
type MyType() =
    let compute(x: int): Async<int> = async { ... }
    member this.ComputeAsTask(x, cancellationToken) = Async.StartAsTask(compute x, cancellationToken)

F# 함수 형식 대신 .NET 대리자 형식 사용

여기서 "F# 함수 형식"은 다음과 같은 int -> int"화살표" 형식을 의미합니다.

대신 다음을 수행합니다.

member this.Transform(f: int->int) =
    ...

방법:

member this.Transform(f: Func<int,int>) =
    ...

F# 함수 형식은 다른 .NET 언어로 class FSharpFunc<T,U> 표시되며 대리자 형식을 이해하는 언어 기능 및 도구에 덜 적합합니다. .NET Framework 3.5 이상을 System.Func 대상으로 하는 상위 메서드를 작성할 때 .NET 개발자가 낮은 마찰 방식으로 이러한 API를 사용할 수 있도록 게시할 수 있는 적절한 API 및 System.Action 대리자가 있습니다. (.NET Framework 2.0을 대상으로 하는 경우 시스템 정의 대리자 형식이 더 제한적입니다. 특정 대리자 형식과 같은 System.Converter<T,U> 미리 정의된 대리자 형식을 사용하거나 정의하는 것이 좋습니다.)

반대로 F#을 향한 라이브러리에는 .NET 대리자가 자연적이지 않습니다(F#연결 라이브러리의 다음 섹션 참조). 따라서 vanilla .NET 라이브러리에 대한 상위 메서드를 개발할 때 일반적인 구현 전략은 F# 함수 형식을 사용하여 모든 구현을 작성한 다음 실제 F# 구현 위에 씬 외관으로 대리자를 사용하여 공용 API를 만드는 것입니다.

F# 옵션 값을 반환하는 대신 TryGetValue 패턴을 사용하고 F# 옵션 값을 인수로 사용하는 방법 오버로드를 선호합니다.

API의 F# 옵션 형식에 대한 일반적인 사용 패턴은 표준 .NET 디자인 기술을 사용하여 vanilla .NET API에서 더 잘 구현됩니다. F# 옵션 값을 반환하는 대신 "TryGetValue" 패턴과 같이 bool 반환 형식과 out 매개 변수를 사용하는 것이 좋습니다. 또한 F# 옵션 값을 매개 변수로 사용하는 대신 메서드 오버로드 또는 선택적 인수를 사용하는 것이 좋습니다.

member this.ReturnOption() = Some 3

member this.ReturnBoolAndOut(outVal: byref<int>) =
    outVal <- 3
    true

member this.ParamOption(x: int, y: int option) =
    match y with
    | Some y2 -> x + y2
    | None -> x

member this.ParamOverload(x: int) = x

member this.ParamOverload(x: int, y: int) = x + y

.NET 컬렉션 인터페이스 형식 IEnumerableT<> 및 IDictionaryKey< 사용,> 매개 변수 값 및 반환 값

.NET 배열T[], F# list<T>Map<Key,Value> 형식 및 Set<T>.NET 콘크리트 컬렉션 형식과 같은 구체적인 컬렉션 형식Dictionary<Key,Value>을 사용하지 않도록 합니다. .NET 라이브러리 디자인 지침에는 다음과 같은 IEnumerable<T>다양한 컬렉션 형식을 사용하는 경우에 대한 유용한 조언이 있습니다. 일부 배열(T[])의 사용은 성능상의 이유로 일부 경우에 허용됩니다. 특히 F seq<T> # 별칭 IEnumerable<T>에 불과하므로 seq는 종종 vanilla .NET API에 적합한 형식입니다.

F# 목록 대신:

member this.PrintNames(names: string list) =
    ...

F# 시퀀스 사용:

member this.PrintNames(names: seq<string>) =
    ...

단위 형식을 메서드의 유일한 입력 형식으로 사용하여 인수 0 메서드를 정의하거나 void 반환 메서드를 정의하는 유일한 반환 형식으로 사용합니다.

단위 형식의 다른 사용을 방지합니다. 다음과 같은 것이 좋습니다.

✔ member this.NoArguments() = 3

✔ member this.ReturnVoid(x: int) = ()

이것은 나쁜:

member this.WrongUnit( x: unit, z: int) = ((), ())

vanilla .NET API 경계에서 null 값 확인

F# 구현 코드는 변경할 수 없는 디자인 패턴과 F# 형식에 null 리터럴 사용에 대한 제한으로 인해 null 값이 적은 경향이 있습니다. 다른 .NET 언어는 null을 훨씬 더 자주 값으로 사용하는 경우가 많습니다. 따라서 vanilla .NET API를 노출하는 F# 코드는 API 경계에서 null에 대한 매개 변수를 확인하고 이러한 값이 F# 구현 코드로 더 깊이 흐르지 않도록 해야 합니다. 패턴에 isNull 대한 함수 또는 패턴 일치를 null 사용할 수 있습니다.

let checkNonNull argName (arg: obj) =
    match arg with
    | null -> nullArg argName
    | _ -> ()

let checkNonNull` argName (arg: obj) =
    if isNull arg then nullArg argName
    else ()

튜플을 반환 값으로 사용하지 않도록 방지

대신 집계 데이터를 보유하는 명명된 형식을 반환하거나 매개 변수를 사용하여 여러 값을 반환하는 것이 좋습니다. 튜플과 구조체 튜플은 .NET에 있지만(구조체 튜플에 대한 C# 언어 지원 포함) .NET 개발자에게 이상적인 예상 API를 제공하지 않는 경우가 많습니다.

매개 변수의 커리 사용 방지

대신 .NET 호출 규칙을 Method(arg1,arg2,…,argN)사용합니다.

member this.TupledArguments(str, num) = String.replicate num str

팁: .NET 언어에서 사용할 라이브러리를 디자인하는 경우 실제로 일부 실험적 C# 및 Visual Basic 프로그래밍을 수행하여 라이브러리가 이러한 언어에서 "올바른 느낌"을 갖도록 할 수 있습니다. .NET Reflector 및 Visual Studio 개체 브라우저와 같은 도구를 사용하여 라이브러리 및 해당 설명서가 개발자에게 예상대로 표시되도록 할 수도 있습니다.

부록

다른 .NET 언어에서 사용할 F# 코드를 디자인하는 엔드 투 엔드 예제

다음 클래스를 고려합니다.

open System

type Point1(angle,radius) =
    new() = Point1(angle=0.0, radius=0.0)
    member x.Angle = angle
    member x.Radius = radius
    member x.Stretch(l) = Point1(angle=x.Angle, radius=x.Radius * l)
    member x.Warp(f) = Point1(angle=f(x.Angle), radius=x.Radius)
    static member Circle(n) =
        [ for i in 1..n -> Point1(angle=2.0*Math.PI/float(n), radius=1.0) ]

이 클래스의 유추된 F# 형식은 다음과 같습니다.

type Point1 =
    new : unit -> Point1
    new : angle:double * radius:double -> Point1
    static member Circle : n:int -> Point1 list
    member Stretch : l:double -> Point1
    member Warp : f:(double -> double) -> Point1
    member Angle : double
    member Radius : double

다른 .NET 언어를 사용하여 프로그래머에게 이 F# 형식이 어떻게 표시되는지 살펴보겠습니다. 예를 들어 대략 C# "서명"은 다음과 같습니다.

// C# signature for the unadjusted Point1 class
public class Point1
{
    public Point1();

    public Point1(double angle, double radius);

    public static Microsoft.FSharp.Collections.List<Point1> Circle(int count);

    public Point1 Stretch(double factor);

    public Point1 Warp(Microsoft.FSharp.Core.FastFunc<double,double> transform);

    public double Angle { get; }

    public double Radius { get; }
}

여기서 F#이 구문을 나타내는 방법에 대해 알아야 할 몇 가지 중요한 사항이 있습니다. 예를 들면 다음과 같습니다.

  • 인수 이름과 같은 메타데이터가 유지되었습니다.

  • 두 인수를 사용하는 F# 메서드는 두 인수를 사용하는 C# 메서드가 됩니다.

  • 함수 및 목록은 F# 라이브러리의 해당 형식에 대한 참조가 됩니다.

다음 코드에서는 이러한 사항을 고려하도록 이 코드를 조정하는 방법을 보여줍니다.

namespace SuperDuperFSharpLibrary.Types

type RadialPoint(angle:double, radius:double) =

    /// Return a point at the origin
    new() = RadialPoint(angle=0.0, radius=0.0)

    /// The angle to the point, from the x-axis
    member x.Angle = angle

    /// The distance to the point, from the origin
    member x.Radius = radius

    /// Return a new point, with radius multiplied by the given factor
    member x.Stretch(factor) =
        RadialPoint(angle=angle, radius=radius * factor)

    /// Return a new point, with angle transformed by the function
    member x.Warp(transform:Func<_,_>) =
        RadialPoint(angle=transform.Invoke angle, radius=radius)

    /// Return a sequence of points describing an approximate circle using
    /// the given count of points
    static member Circle(count) =
        seq { for i in 1..count ->
                RadialPoint(angle=2.0*Math.PI/float(count), radius=1.0) }

유추된 F# 코드 형식은 다음과 같습니다.

type RadialPoint =
    new : unit -> RadialPoint
    new : angle:double * radius:double -> RadialPoint
    static member Circle : count:int -> seq<RadialPoint>
    member Stretch : factor:double -> RadialPoint
    member Warp : transform:System.Func<double,double> -> RadialPoint
    member Angle : double
    member Radius : double

이제 C# 서명은 다음과 같습니다.

public class RadialPoint
{
    public RadialPoint();

    public RadialPoint(double angle, double radius);

    public static System.Collections.Generic.IEnumerable<RadialPoint> Circle(int count);

    public RadialPoint Stretch(double factor);

    public RadialPoint Warp(System.Func<double,double> transform);

    public double Angle { get; }

    public double Radius { get; }
}

vanilla .NET 라이브러리의 일부로 사용할 수 있도록 이 형식을 준비하기 위한 수정 사항은 다음과 같습니다.

  • 각각 , nlcounttransformffactorRadialPoint여러 이름을 Point1조정했습니다.

  • 를 사용하여 [ ... ] 목록 생성을 시퀀스 생성으로 변경하는 대신 RadialPoint list 반환 형식 seq<RadialPoint> 을 사용IEnumerable<RadialPoint>했습니다.

  • F# 함수 형식 System.Func 대신 .NET 대리자 형식을 사용했습니다.

이렇게 하면 C# 코드에서 훨씬 더 쉽게 사용할 수 있습니다.