F# コンポーネント デザインのガイドライン

このドキュメントは、F# プログラミングに関する一連のコンポーネント デザイン ガイドラインです。「F# Component Design Guidelines, v14 (F# コンポーネント デザイン ガイドライン バージョン 14)」、Microsoft Research、および F# Software Foundation によって最初にキュレートおよびおよび保守されたバージョンに基づいています。

このドキュメントは、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 コメント (/// comment) または標準の XML コメント (///<summary>comment</summary>) のいずれかを使用できます。

安定したライブラリとコンポーネントの API のために明示的なシグネチャ ファイル (.fsi) の使用を検討する

F# ライブラリに明示的なシグネチャ ファイルを使用すると、パブリック API の概要が簡潔になります。こうすることで、ライブラリのパブリック サーフェス全体を確実に把握し、パブリック ドキュメントと内部実装の詳細を明確に分離できます。 シグネチャ ファイルを使用する場合、実装ファイルとシグネチャ ファイルの両方に変更を加える必要があるため、パブリック API の変更には手間がかかります。 そのため、シグネチャ ファイルを導入するのは、通常、API が固まってきて、大幅な変更が予想されなくなってからにすべきです。

.NET で文字列を使用する場合はベスト プラクティスに従う

プロジェクトのスコープで必要な場合は、.NET の文字列を使用するためのベスト プラクティスのガイダンスに従います。 特に、文字列の変換と比較での "文化的意図" を明示的に示します (該当する場合)。

F# 対応ライブラリのガイドライン

このセクションでは、F# 対応パブリック ライブラリ (つまり、F# 開発者に使用されることを目的としたパブリック API を公開しているライブラリ) を開発する際の推奨事項を示します。 特に F# に適用できるさまざまなライブラリデザインの推奨事項があります。 具体的な推奨事項がここにない場合は、.NET ライブラリ デザイン ガイドラインが予備のガイダンスとなります。

名前付け規則

.NET の名前付け規則と大文字と小文字の規則を使用する

次の表は、.NET の名前付け規則と大文字と小文字の規則に従っています。 F# コンストラクトも含めるために、わずかな追加があります。 これらの推奨事項は特に、F# 同士の境界を超える API を対象としており、.NET BCL および大部分のライブラリの表示形式に適合します。

構成体 ケース パーツ Notes
具象型 パスカル ケース 名詞または形容詞 List、Double、Complex 具象型は、構造体、クラス、列挙体、デリゲート、レコード、および共用体です。 従来、OCaml では型名は小文字ですが、F# には型に .NET の名前付けスキームが採用されています。
DLL パスカル ケース Fabrikam.Core.dll
共用体のタグ パスカル ケース [名詞] Some、Add、Success パブリック API でプレフィックスを使用しないでください。 必要に応じて、内部の場合は "type Teams = TAlpha | TBeta | TDelta" などのプレフィクスを使用します。
Event パスカル ケース 動詞 ValueChanged / ValueChanging
例外 パスカル ケース WebException 名前の末尾は "Exception" にします。
フィールド パスカル ケース [名詞] CurrentName
インターフェイス型 パスカル ケース 名詞または形容詞 IDisposable 名前の先頭は "I" にします。
方法 パスカル ケース 動詞 ToString
名前空間 パスカル ケース Microsoft.FSharp.Core 一般的には <Organization>.<Technology>[.<Subnamespace>] を使用しますが、組織に依存しないテクノロジの場合は、組織を削除します。
パラメーター キャメル ケース [名詞] typeName、transform、range
let 値 (内部) キャメル ケースまたはパスカル ケース 名詞または動詞 getValue、myTable
let 値 (外部) キャメル ケースまたはパスカル ケース 名詞または動詞 List.map、Dates.Today 従来の機能設計パターンに従う場合、let バインド値が発行されることがよくあります。 ただし、その識別子が他の .NET 言語から使用される可能性がある場合は、一般的にパスカル ケースを使用します。
プロパティ パスカル ケース 名詞または形容詞 IsEndOfFile、BackColor 通常、ブール型プロパティには Is と Can が使用されます。また、IsNotEndOfFile ではなく IsEndOfFile のように肯定形にするようにします。

省略形を避ける

.NET ガイドラインでは、省略形の使用は非推奨です (たとえば、"OnBtnClick ではなく OnButtonClick を使用する")。 "Asynchronous" に対する Async など、一般的な省略形は許容されます。 このガイドラインは、関数型プログラミングでは無視されることがあります。たとえば、List.iter には "iterate" の省略形が使用されています。 このため、F# 同士のプログラミングでは省略形の使用が許容される傾向がありますが、パブリック コンポーネントの設計では一般的に避けるべきです。

名前の大文字と小文字による衝突を避ける

.NET ガイドラインによると、一部のクライアント言語 (Visual Basic など) では大文字と小文字が区別されないため、大文字と小文字の違いしかない場合は名前の衝突を解消できません。

適切な場合は頭字語を使用する

XML などの頭字語は省略形ではなく、大文字でない形式 (Xml) で .NET ライブラリで広く使用されています。 よく知られ、広く認識されている頭字語のみを使用するようにします。

ジェネリック パラメーターの名前にはパスカル ケースを使用する

F# 対応ライブラリなど、パブリック API のジェネリック パラメーターの名前にはパスカル ケースを使用してください。 特に、任意のジェネリック パラメーターには TUT1T2 などの名前を使用し、特定の名前が意味を持つ場合は、F# 対応のライブラリには KeyValueArg などの名前を使用します (ただし、TKey などは使用しません)。

F# モジュールのパブリック関数と値にはパスカル ケースまたはキャメル ケースのいずれかを使用する

キャメル ケースは、修飾なしで使用するように設計されたパブリック関数 (invalidArg など) と、"標準のコレクション関数" (List.map など) に使用されます。 どちらの場合も、関数名は言語のキーワードのように機能します。

オブジェクト、型、モジュールの設計

名前空間またはモジュールを使用して型とモジュールを含める

コンポーネント内の各 F# ファイルは、名前空間の宣言またはモジュールの宣言のいずれかで始めるようにします。

namespace Fabrikam.BasicOperationsAndTypes

type ObjectType1() =
    ...

type ObjectType2() =
     ...

module CommonOperations =
    ...

or

module Fabrikam.BasicOperationsAndTypes

type ObjectType1() =
    ...

type ObjectType2() =
    ...

module CommonOperations =
    ...

モジュールと名前空間を使用して最上位レベルでコードを整理する場合の違いは次のとおりです。

  • 名前空間は、複数のファイルにまたがることができます
  • 名前空間は、内部モジュール内にある場合を除き、F# 関数を含むことはできません
  • あらゆるモジュールのコードは、1 つのファイルに含まれている必要があります
  • 最上位レベルのモジュールには、内部モジュールを必要とせず、F# 関数を含めることができます

最上位レベルの名前空間とモジュールのどちらを選択するかは、コードのコンパイル形式に影響します。そのため、万が一、F# コードの外部で API が使用されることになった場合に、他の .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
}

インターフェイスは .NET で最重要視される概念であり、これを使用して、通常はファンクターを使用するものを実現できます。 さらに、それらを使用して、関数のレコードでは不可能な実存型をプログラムにエンコードすることもできます。

コレクションに対して機能する関数をグループするためにモジュールを使用する

コレクション型を定義するときは、新しいコレクション型に CollectionType.mapCollectionType.iter などの標準的な操作セットを指定することを検討してください。

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

このようなモジュールを含める場合は、FSharp.Core にある関数の標準の名前付け規則に従ってください。

一般的な正規関数については、数学と DSL のライブラリの場合は特に、モジュールを使用して関数をグループ化してください。

たとえば、Microsoft.FSharp.Core.Operators は、FSharp.Core.dll によって提供されている最上位レベルの関数 (abssin など) の自動的に開かれるコレクションです。

同様に、統計ライブラリには、関数 erferfc を備えたモジュールが含まれている場合があります。このモジュールは、明示的に、または自動的に開かれるように設計されています。

RequireQualifiedAccess の使用を検討し、AutoOpen 属性を慎重に適用する

モジュールに [<RequireQualifiedAccess>] 属性を追加すると、モジュールを開くことができないこと、およびモジュールの要素への参照には明示的な修飾アクセスが必要であることが示されます。 たとえば、Microsoft.FSharp.Collections.List モジュールにはこの属性があります。

これは、モジュールの関数と値の名前が、他のモジュールの名前と競合する可能性がある場合に便利です。 修飾されたアクセスを要求すると、ライブラリの長期的な保守性と発展性を大幅に向上させることができます。

FSharp.Core によって提供されるもの (SeqListArray など) を拡張するカスタム モジュールには [<RequireQualifiedAccess>] 属性を持たせることを強くお勧めします。これらのモジュールは、F# コードで一般的に使用され、[<RequireQualifiedAccess>] が定義されているためです。より一般的には、属性を持たないカスタム モジュールにより、属性を持つ他のモジュールがシャドウまたは拡張されたりする場合は、属性を持たないものを定義することはお勧めしません。

[<AutoOpen>] 属性をモジュールに追加すると、それを格納する名前空間が開かれたときに、そのモジュールも開かれることになります。 [<AutoOpen>] 属性をアセンブリに適用して、アセンブリが参照されたときに自動的に開かれるモジュールを示すこともできます。

たとえば、統計ライブラリ MathsHeaven.Statistics には、関数 erferfc を含む module MathsHeaven.Statistics.Operators が含まれているとします。 このモジュールを [<AutoOpen>] としてマークするのは妥当です。 これは、open MathsHeaven.Statistics の場合もこのモジュールが開かれ、名前 erferfc がスコープに入ることを意味します。 拡張メソッドを含むモジュールに対して [<AutoOpen>] を使用することもお勧めです。

[<AutoOpen>] を使いすぎると名前空間が汚染されるため、この属性は慎重に使用する必要があります。 特定のドメインの特定のライブラリの場合、[<AutoOpen>] を適切に使用すると、使いやすさが向上する可能性があります。

既知の演算子を使用することが適切な場合は、クラスに演算子メンバーを定義することを検討する

ベクトルなどの数学的なコンストラクトをモデル化するためにクラスが使用されることがあります。 モデル化されるドメインに既知の演算子がある場合、それらを固有のメンバーとしてクラスに定義すると便利です。

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# のコーディングでは、これらの型を List.sumBy のようなメンバー制約のある F# の関数とメソッドと組み合わせて使用できるため、さらに重要になります。

CompiledName を使用して、他の .NET 言語のコンシューマーに .NET フレンドリな名前を提供することを検討する

F# コンシューマー向けに 1 つのスタイルで名前を付け (たとえば、静的メンバーを小文字にしてモジュールにバインドされた関数のように見せるなど)、アセンブリにコンパイルされるときには別のスタイルの名前を付けたい場合があります。 [<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

多くのコンポーネントを含む戻り値の型の場合、またはコンポーネントが 1 つの識別可能なエンティティに関連している場合は、タプルではなく名前付きの型を使用することを検討してください。

F# API の境界での非同期プログラミングには Async<T> を使用する

T を返す Operation という名前の対応する同期操作がある場合、非同期操作には、Async<T> を返す場合は AsyncOperationTask<T> を返す場合は OperationAsync という名前を付けるようにします。 Begin/End メソッドを公開している、一般的に使用されている .NET 型の場合は、それらの .NET API に F# の非同期プログラミング モデルを提供するファサードとして、Async.FromBeginEnd を使用して拡張メソッドを書くことを検討してください。

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# 同士のコンポーネントへの F# 拡張メンバーの適用は慎重にする

F# 拡張メンバーは、一般的には、その使用されているモードの大部分で、型に関連する組み込み操作のクロージャ内にある操作にのみ使用すべきです。 一般的な用途の 1 つは、さまざまな .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>] を使用する

判別共用体のケースのように、同じ名前が、別のものには最適な名前になるドメインがあることに気付くことがあります。 [<RequireQualifiedAccess>] を使用してケース名を明確にし、open ステートメントの順序に依存するシャドウイングによる紛らわしいエラーのトリガーを回避できます。

判別共用体の設計が進化する可能性がある場合は、バイナリ互換 API のために、それらの型を隠す

共用体の型は、簡潔なプログラミング モデルの F# パターン マッチング形式に依存しています。 前述のように、このような型の設計が進化する可能性がある場合は、具象データ表現を明らかにすることは避けるべきです。

たとえば、判別共用体の表現は、非公開または内部の宣言を使用するか、シグネチャ ファイルを使用して隠すことができます。

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

判別共用体を無差別に明らかにすると、ユーザー コードを壊すことなくライブラリのバージョンを管理することが困難になる可能性があります。 その代わりに、1 つ以上のアクティブ パターンを明らかにして、型の値に対するパターン マッチングを許可することを検討してください。

アクティブ パターンは、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# 同士のライブラリ デザインには使用しないでください。 これは、なじみのない、または非標準の暗黙的な制約に基づくライブラリ デザインは、ユーザー コードが柔軟性を失い、ある特定のフレームワーク パターンに縛られる傾向があるからです。

さらに、このようにメンバー制約を多用すると、コンパイル時間が非常に長くなる可能性があります。

演算子の定義

カスタムのシンボリック演算子を定義しない

カスタムの演算子は、状況によっては不可欠であり、大規模な実装コード内では非常に便利な表記手段です。 ライブラリの新規ユーザーにとって、名前付き関数の方が使いやすいことはよくあります。 さらに、カスタムのシンボリック演算子を文書化するのは難しい場合があります。また、IDE と検索エンジンには既存の制限があるため、ユーザーが演算子のヘルプを調べることが困難になる場合があります。

そのため、名前付きの関数とメンバーとして機能を公開し、さらにその機能のための演算子を公開するのは、表記上の利点が、ドキュメント作成とそれらを用意する認知コストを上回る場合にのみにするのが最適です。

測定単位

F# コードのタイプ セーフを高めるために測定単位は慎重に使用する

他の .NET 言語から見た場合、測定単位の追加の入力情報は消去されます。 .NET コンポーネント、ツール、およびリフレクションからは "単位のない型" が見えることに注意してください。 たとえば、C# のコンシューマーからは float<kg> ではなく float が見えます。

型略称

F# コードを簡素化するために、型の省略形は慎重に使用する

.NET コンポーネント、ツール、およびリフレクションからは、型の省略名は見えません。 また、型の省略形を多用すると、ドメインが実際よりも複雑に見えてしまい、コンシューマーを混乱させる可能性があります。

メンバーとプロパティが、省略されている型で使用できるものと本質的に異なる場合、パブリック型には型の省略形を使用しない

この場合、省略されている型によって、定義されている実際の型の表現についてあまりにも多くのことが明らかになります。 その代わりに、省略形をクラス型または単一ケースの判別共用体でラップすることを検討してください (または、パフォーマンスが重要な場合は、構造体型を使用して省略形をラップすることを検討してください)。

たとえば、マルチマップを F# マップの特殊なケースとして定義したい場合があります。次に例を示します。

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

ただし、この型に対する論理的なドット表記の操作は、マップに対するの操作と同じではありません。たとえば、ルックアップ演算子 map[key] の場合、キーが辞書にない場合、例外を発生させるのではなく、空の一覧を返すのが合理的です。

他の .NET 言語から使用されるライブラリのガイドライン

他の .NET 言語から使用されるライブラリを設計する場合は、.NET ライブラリ デザイン ガイドラインに従うことが重要です。 このドキュメントでは、F# のコンストラクトを制限なく使用する F# 対応のライブラリとは異なり、このようなライブラリのことをバニラ .NET ライブラリと表記します。 バニラ .NET ライブラリを設計するということは、パブリック API での F# 固有のコンストラクトの使用を最小限に抑え、他の .NET Framework と整合性のある、使い慣れた慣用的な API を提供することを意味します。 この規則については、次のセクションで説明します。

名前空間と型の設計 (他の .NET 言語から使用されるライブラリの場合)

コンポーネントのパブリック API には .NET の名前付け規則を適用する

省略名の使用と .NET の大文字と小文字のガイドラインには特に注意してください。

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

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

コンポーネントの主要な組織構造として、名前空間、型、メンバーを使用する

パブリック機能を含むすべてのファイルは、namespace の宣言から始めます。また、名前空間内の公開されているエンティティは型のみにします。 F# モジュールは使用しないでください。

実装コード、ユーティリティの型、ユーティリティ関数の保持には、非パブリック モジュールを使用します。

モジュールよりも、静的な型を使用することをお勧めします。これは、今後、F# モジュール内では使用できないオーバーロードやその他の .NET API 設計の概念を使用できるように、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

型の設計が進化しない場合は、バニラ .NET API で F# のレコード型を使用する

F# のレコード型は、シンプルな .NET クラスにコンパイルされます。 これらは、API 内の一部のシンプルで安定した型に適しています。 [<NoEquality>][<NoComparison>] の属性を使用して、インターフェイスの自動生成を抑制することを検討してください。 また、パブリック フィールドが公開されるため、バニラ .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; }
}

バニラ .NET API では F# の共用体型の表現を隠す

F# 同士のコーディングであっても、F# の共用体型はコンポーネントの境界を越えて共通で使用されることはありません。 これらは、コンポーネントやライブラリ内で内部的に使用される場合には優れた実装手段となります。

バニラ .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 とその他のコンポーネントの設計

WinForms、WPF、ASP.NET など、.NET 内で使用できるさまざまなフレームワークがあります。 このようなフレームワークで使用するコンポーネントを設計する場合は、それぞれの名前付け規則と設計規則を使用する必要があります。 たとえば、WPF プログラミングの場合、設計するクラスに WPF の設計パターンを採用します。 ユーザー インターフェイス プログラミングのモデルの場合、イベントなどの設計パターンや、System.Collections.ObjectModel にあるような通知ベースのコレクションを使用します。

オブジェクトとメンバーの設計 (他の .NET 言語から使用されるライブラリの場合)

CLIEvent 属性を使用して .NET イベントを公開する

オブジェクトと (既定で FSharpHandler 型のみを使用する Event ではなく) EventArgs を受け取る特定の .NET デリゲート型を使用して DelegateEvent を構築することで、他の .NET 言語になじみのある方法でイベントを発行します。

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.FuncSystem.Action のデリゲートは、.NET 開発者がこれらの API を少ない抵抗で使用できるように発行する場合に適した API です (.NET Framework 2.0 を対象とする場合、システム定義のデリゲート型はさらに制限されます。System.Converter<T,U> などの事前に定義されたデリゲート型を使用するか、特定のデリゲート型を定義することを検討してください)。

反対に、.NET のデリゲートは、F# 対応のライブラリには向いていません (F# 対応のライブラリに関する次のセクションを参照してください)。 そのため、バニラ .NET ライブラリの高次メソッドを開発する場合の一般的な実装戦略は、F# 関数型を使用してすべての実装を作成し、実際の F# の実装の上に薄いファサードとしてデリゲートを使用してパブリック API を作成することです。

F# のオプション値を返すのではなく、TryGetValue パターンを使用し、引数として F# オプション値を受け取るのではなく、メソッドのオーバーロードを優先する

API で F# のオプション型を使用する一般的なパターンは、標準の .NET 設計手法を使用するバニラ .NET API の方が適切に実装できます。 F# のオプション値を返すのではなく、"TryGetValue" パターンのように、ブールの戻り値の型と 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 コレクション インターフェイスの型 IEnumerable<T> と IDictionary<Key,Value> を使用する

.NET の配列 T[]、F# の型 list<T>Map<Key,Value>Set<T> などの具象コレクション型と、Dictionary<Key,Value> などの .NET の具象コレクション型の使用は避けてください。 .NET ライブラリ デザイン ガイドラインには、IEnumerable<T> などのさまざまなコレクションの型をいつ使用するかについての適切なアドバイスがあります。 配列 (T[]) の使用は、パフォーマンス上の理由から、状況によっては許容されます。 特に、seq<T>IEnumerable<T> の F# の別名にすぎないため、多くの場合、seq はバニラ .NET API に適した型であることに注意してください。

推奨されない F# の一覧の例:

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

次のように F# のシーケンスを使用してください。

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

引数なしのメソッドを定義するためにメソッドの唯一の入力型として、または void を返すメソッドを定義するための唯一の戻り値の型として、unit 型を使用する

unit 型の他の用途は避けてください。 良い例:

✔ member this.NoArguments() = 3

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

悪い例:

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

バニラ .NET API の境界で null 値を確認する

F# の実装コードは、不変の設計パターンと F# の型に対する null リテラルの使用制限があるため、null 値が少なくなる傾向があります。 他の .NET 言語では、null を値として使用することがよくあります。 このため、バニラ .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 Object Browser などのツールを使用して、ライブラリとそのドキュメントが開発者にとって期待どおりに表示されることを確認することもできます。

付録

他の .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

この F# の型が、別の .NET 言語を使用しているプログラマからはどのように見えるかを見てみましょう。 たとえば、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# がどのようにコンストラクトを表現しているかについて、いくつか重要なポイントがあります。 次に例を示します。

  • 引数名などのメタデータは保持されます。

  • 2 つの引数を受け取る F# メソッドは、2 つの引数を受け取る 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; }
}

この型をバニラ .NET ライブラリの一部として使用できるようにするために行った修正は次のとおりです。

  • いくつかの名前を調整しました。Point1nlf は、それぞれ RadialPointcountfactortransform になりました。

  • [ ... ] を使用した一覧の構造を IEnumerable<RadialPoint> を使用したシーケンス構造に変更することにより、RadialPoint list ではなく seq<RadialPoint> の戻り値の型を使用しました。

  • F# の関数型ではなく .NET のデリゲート型 System.Func を使用しました。

こうすることで、C# コードではるかに使いやすくなりました。