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 コメント (/// comment
) または標準 XML コメント (///<summary>comment</summary>
) を使用できます。
安定したライブラリとコンポーネント API に明示的な署名ファイル (.fsi) を使用することを検討してください
F# ライブラリで明示的な署名ファイルを使用すると、パブリック API の簡潔な概要が提供されます。これにより、ライブラリの完全なパブリック サーフェスを確実に把握し、パブリック ドキュメントと内部実装の詳細を明確に分離できます。 署名ファイルは、実装ファイルと署名ファイルの両方で変更を行う必要があり、パブリック API の変更に摩擦を加えます。 その結果、署名ファイルは通常、API が固まり、大幅な変更が予想されなくなった場合にのみ導入する必要があります。
.NET で文字列を使用するためのベスト プラクティスに従う
プロジェクトの範囲がそれを保証する場合は、.NET の文字列を使用するためのベスト プラクティス に従ってください。 特に、文字列の変換と比較における文化的意図が明示的に示されています (該当する場合)。
F# に接続するライブラリのガイドライン
このセクションでは、パブリック F# に接続するライブラリを開発するための推奨事項について説明します。つまり、F# 開発者が使用することを目的としたパブリック API を公開するライブラリです。 特に F# に適用できるライブラリ設計に関するさまざまな推奨事項があります。 具体的な推奨事項がない場合は、.NET ライブラリの設計ガイドラインがフォールバック ガイダンスです。
名前付け規則
.NET の名前付け規則と大文字と小文字の表記規則を使用する
次の表は、.NET の名前付け規則と大文字と小文字の表記規則に従います。 F# コンストラクトも含める小さな追加機能があります。 これらの推奨事項は特に、F#から F# の境界を超える API を対象としており、.NET BCL のイディオムと大部分のライブラリに適合します。
構成体 | ケース | パーツ | 使用例 | メモ |
---|---|---|---|---|
具象型 | PascalCase | 名詞/形容詞 | List、Double、Complex | 具象型は、構造体、クラス、列挙型、デリゲート、レコード、および共用体です。 OCaml では、型名は従来小文字ですが、F# では型に .NET 名前付けスキームが採用されています。 |
DLL | PascalCase | Fabrikam.Core.dll | ||
共用体のタグ | PascalCase | 名詞 | Some、Add、Success | パブリック API ではプレフィックスを使用しないでください。 必要に応じて、内部で使用する場合は、"type Teams = TAlpha | TBeta | TDelta" のようにプレフィックスを付けてください。 |
出来事 | PascalCase | 動詞 | 値変更済み/値変更中 | |
例外 | PascalCase | WebException | 名前は "Exception" で終わる必要があります。 | |
フィールド | PascalCase | 名詞 | 現在の名前 | |
インターフェイスの種類 | PascalCase | 名詞/形容詞 | IDisposable | 名前は "I" で始まる必要があります。 |
方式 | PascalCase | 動詞 | ToString | |
Namespace | PascalCase | Microsoft.FSharp.Core | 一般的に <Organization>.<Technology>[.<Subnamespace>] を使用しますが、テクノロジが組織から独立している場合は組織を削除します。 |
|
パラメーター | camelCase | 名詞 | typeName、transform、range | |
let 値 (内部) | camelCase または PascalCase | 名詞/動詞 | getValue、myTable | |
let 値 (外部) | camelCase または PascalCase | 名詞/動詞 | List.map、Dates.Today | let バインド値は、多くの場合、従来の機能設計パターンに従うときに公開されます。 ただし、一般に、識別子を他の .NET 言語から使用できる場合は、PascalCase を使用します。 |
財産 | PascalCase | 名詞/形容詞 | IsEndOfFile、BackColor | ブール型プロパティは、IsNotEndOfFile ではなく IsEndOfFile のように、一般的に Is と Can を使用し、肯定的である必要があります。 |
省略形を避ける
.NET のガイドラインでは、省略形の使用は推奨されません (たとえば、"OnBtnClick
ではなく OnButtonClick
を使用する" など)。 "非同期" の Async
など、一般的な省略形は許容されます。 このガイドラインは、関数型プログラミングでは無視されることがあります。たとえば、List.iter
は "iterate" の省略形を使用します。 このため、F# から F# へのプログラミングでは省略形の使用が許容される傾向がありますが、一般的にはパブリック コンポーネントの設計では避ける必要があります。
名前の大文字と小文字による衝突を避ける
.NET ガイドラインによると、一部のクライアント言語 (Visual Basic など) では大文字と小文字が区別されないため、大文字と小文字の違いしかない場合は名前の衝突を解消できません。
必要に応じて頭字語を使用する
XML などの頭字語は省略形ではなく、.NET ライブラリ (Xml) で広く使用されています。 よく知られている、広く認識されている頭字語のみを使用する必要があります。
ジェネリック パラメーター名に PascalCase を使用する
パブリック API のジェネリック パラメーター名には PascalCase を使用してください (F# に接続するライブラリを含む)。 特に、任意のジェネリック パラメーターには T
、U
、T1
、T2
などの名前を使用し、特定の名前が意味を持つ場合は、F# に接続するライブラリでは、Key
、Value
、Arg
などの名前を使用します (TKey
などではありません)。
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# 関数を含めることはできません
- 特定のモジュールのコードは、1 つのファイル内に含まれている必要があります
- 最上位モジュールには、内部モジュールを必要とせずに 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
}
インターフェイスは .NET の最上位の概念であり、Functor が通常提供するものを実現するために使用できます。 さらに、プログラムで関数のレコードではできない存在型をエンコードするために使用できます。
モジュールを使用してコレクションに作用する関数をグループ化する
コレクション型を定義する場合は、新しいコレクション型の CollectionType.map
や CollectionType.iter
) などの標準の操作セットを提供することを検討してください。
module CollectionType =
let map f c =
...
let iter f c =
...
このようなモジュールを含める場合は、FSharp.Core で見つかった関数の標準の名前付け規則に従います。
モジュールを使用して、一般的な正規関数 (特に数学および DSL ライブラリ) の関数をグループ化する
たとえば、Microsoft.FSharp.Core.Operators
は、FSharp.Core.dllによって提供される最上位の関数 (abs
や sin
など) の自動的に開かれたコレクションです。
同様に、統計ライブラリには、関数 erf
と erfc
を含むモジュールが含まれる場合があります。このモジュールは、明示的または自動的に開かれるように設計されています。
RequireQualifiedAccess の使用を検討し、AutoOpen 属性を慎重に適用する
モジュールに [<RequireQualifiedAccess>]
属性を追加すると、モジュールを開くことができない可能性があり、モジュールの要素への参照には明示的な修飾アクセスが必要であることが示されます。 たとえば、Microsoft.FSharp.Collections.List
モジュールにはこの属性があります。
これは、モジュール内の関数と値の名前が他のモジュールの名前と競合する可能性がある場合に便利です。 修飾されたアクセスを要求すると、ライブラリの長期的な保守性と進化性が大幅に向上する可能性があります。
FSharp.Core
によって提供されるモジュール (Seq
、List
、Array
など) を拡張するカスタム モジュールの [<RequireQualifiedAccess>]
属性は、F# コードで一般的に使用され、[<RequireQualifiedAccess>]
定義されているため、強くお勧めします。より一般的には、そのようなモジュールがシャドウしたり、属性を持つ他のモジュールを拡張したりする場合は、属性を持たないカスタム モジュールを定義することはお勧めしません。
[<AutoOpen>]
属性をモジュールに追加すると、含まれている名前空間が開かれたときにモジュールが開かれます。 [<AutoOpen>]
属性をアセンブリに適用して、アセンブリが参照されたときに自動的に開かれるモジュールを示すこともできます。
たとえば、MathsHeaven.Statistics 統計ライブラリには、erf
および erfc
関数を含む module MathsHeaven.Statistics.Operators
が含まれている場合があります。 このモジュールを [<AutoOpen>]
としてマークすることは妥当です。 つまり、open MathsHeaven.Statistics
もこのモジュールを開き、名前 erf
と erfc
をスコープに取り込みます。 [<AutoOpen>]
のもう 1 つの優れた用途は、拡張メソッドを含むモジュールです。
[<AutoOpen>]
の過剰使用は、汚染された名前空間につながるので、属性は慎重に使用する必要があります。 特定のドメイン内の特定のライブラリでは、[<AutoOpen>]
を慎重に使用すると、使いやすさが向上する可能性があります。
既知の演算子を使用することが適切なクラスで演算子メンバーを定義することを検討する
Vector などの数学的構造をモデル化するためにクラスが使用される場合があります。 モデル化されるドメインに既知の演算子がある場合は、クラスに組み込まれているメンバーとして定義すると便利です。
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 など) と組み合わせて使用できるため、さらに重要な場合があります。
他の .NET 言語の利用者に対して、.NET で扱いやすい名前を提供するために、CompiledName を使用することを検討してください。
場合によっては、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>
を返す場合は AsyncOperation
、Task<T>
を返す場合は OperationAsync
する必要があります。 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# 同士のコンポーネントへの 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# メンバー制約を使用して、"duck typing" をシミュレートできます。 ただし、これを利用するメンバーは、通常、F# 同士のライブラリ デザインには使用しないでください。 これは、未知の暗黙的制約または非標準の暗黙的制約に基づくライブラリ 設計では、ユーザー コードが柔軟性を持たなくなり、特定のフレームワーク パターンに関連付けられている傾向があるためです。
さらに、この方法でメンバー制約を頻繁に使用すると、コンパイル時間が非常に長くなることがあります。
演算子の定義
カスタム シンボリック演算子の定義を避ける
カスタム演算子は状況によっては不可欠であり、実装コードの大規模な本文内で非常に便利な表記デバイスです。 ライブラリの新しいユーザーの場合、名前付き関数の方が使いやすいことがよくあります。 さらに、カスタムシンボリック演算子は文書化するのが難しい場合があり、IDE と検索エンジンの既存の制限により、ユーザーは演算子のヘルプを検索するのが難しくなります。
その結果、機能を名前付き関数およびメンバーとして公開し、さらに、表記上の利点がそれらを持つことのドキュメントとコグニティブ コストを上回る場合にのみ、この機能の演算子を公開することをお勧めします。
測定単位
F# コードにおける型の安全性を高めるために、単位を慎重に使用する
他の .NET 言語で表示すると、測定単位に関する追加の入力情報は消去されます。 .NET コンポーネント、ツール、およびリフレクションでは、単位なしの型が表示されることに注意してください。 たとえば、C# コンシューマーには、float<kg>
ではなく float
が表示されます。
型略称
型の省略形を慎重に使用して F# コードを簡略化する
.NET コンポーネント、ツール、リフレクションには、型の省略名は表示されません。 型の省略形を大幅に使用すると、ドメインが実際よりも複雑に見える可能性があり、コンシューマーを混乱させる可能性があります。
メンバーとプロパティが、省略されている型で使用できるものと本質的に異なる場合、パブリック型には型の省略形を使用しない
この場合、省略される型は、定義されている実際の型の表現に関してあまりにも多くを明らかにします。 代わりに、省略形をクラス型または単一ケースの判別共用体でラップすることを検討してください (または、パフォーマンスが不可欠な場合は、構造体型を使用して省略形をラップすることを検討してください)。
たとえば、F# マップの特殊なケースとしてマルチマップを定義したくなる場合があります。次に例を示します。
type MultiMap<'Key,'Value> = Map<'Key,'Value list>
ただし、この型に対する論理ドット表記演算は、Map での操作と同じではありません。たとえば、キーがディクショナリにない場合は、ルックアップ演算子 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# モジュールは使用しないでください。
非パブリック モジュールを使用して、実装コード、ユーティリティ型、およびユーティリティ関数を保持します。
静的型はモジュールよりも優先する必要があります。これにより、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
型の設計が進化しない場合は、バニラ .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#から 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 やその他のコンポーネントを設計する
.NET には、WinForms、WPF、ASP.NET など、さまざまなフレームワークがあります。 これらのフレームワークで使用するコンポーネントを設計する場合は、それぞれの名前付け規則と設計規則を使用する必要があります。 たとえば、WPF プログラミングの場合は、設計するクラスに WPF デザイン パターンを採用します。 ユーザー インターフェイス プログラミングのモデルの場合は、イベントや通知ベースのコレクションなどの設計パターン (System.Collections.ObjectModelで見つかったものなど) を使用します。
オブジェクトとメンバーのデザイン (他の .NET 言語から使用するライブラリ用)
CLIEvent 属性を使用して .NET イベントを公開する
オブジェクトと EventArgs
を受け取る特定の .NET デリゲート型 (既定で FSharpHandler
型のみを使用する Event
ではなく) を使用して 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>
オブジェクトよりもコンポジションが少なくなります。これは、"既に実行されている" タスクを表し、並列コンポジションを実行する方法や、キャンセルシグナルやその他のコンテキスト パラメーターの伝達を隠す方法では一緒に構成できないためです。
ただし、これに関わらず、Tasks を返すメソッドは、.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 以降を対象とする上位のメソッドを作成する場合、.NET 開発者がこれらの API を低摩擦で使用できるようにするために、System.Func
および System.Action
デリゲートが発行に適した 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」パターンのように、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 コレクション インターフェイス型 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 を返すメソッドを定義する唯一の戻り値の型として、単位型を使用します。
ユニット型の他の使用は避けてください。 これらは適切です。
✔ 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# 実装コードに深く入らないようにする必要があります。 null
パターンの isNull
関数またはパターン マッチングを使用できます。
let checkNonNull argName (arg: obj) =
match arg with
| null -> nullArg argName
| _ -> ()
let checkNonNull' argName (arg: obj) =
if isNull arg then nullArg argName
else ()
F# 9 以降では、新しい | null
構文 を利用して、コンパイラが可能な null 値と、処理が必要な場所を示すことができます。
let checkNonNull argName (arg: obj | null) =
match arg with
| null -> nullArg argName
| _ -> ()
let checkNonNull' argName (arg: obj | null) =
if isNull arg then nullArg argName
else ()
F# 9 では、可能性のある null 値が処理されないことが検出されると、コンパイラによって警告が出力されます。
let printLineLength (s: string) =
printfn "%i" s.Length
let readLineFromStream (sr: System.IO.StreamReader) =
// `ReadLine` may return null here - when the stream is finished
let line = sr.ReadLine()
// nullness warning: The types 'string' and 'string | null'
// do not have equivalent nullability
printLineLength line
これらの警告には、マッチングで F# の null パターンを使用して対処する必要があります。
let printLineLength (s: string) =
printfn "%i" s.Length
let readLineFromStream (sr: System.IO.StreamReader) =
let line = sr.ReadLine()
match line with
| null -> ()
| s -> printLineLength s
戻り値としてタプルを使用しないようにする
代わりに、集計データを保持する名前付き型を返すか、out パラメーターを使用して複数の値を返します。 タプルと構造体タプルは .NET に存在します (構造体タプルの C# 言語サポートを含む)、ほとんどの場合、.NET 開発者にとって理想的で期待される API は提供されません。
パラメーターのカリー化を使用しない
代わりに、Method(arg1,arg2,…,argN)
.NET 呼び出し規則を使用します。
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# がコンストラクトをどのように表しているかについて注意すべき重要な点がいくつかあります。 例えば:
引数名などのメタデータは保持されています。
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 ライブラリの一部として使用するためにこの型を準備するために行われた修正は次のとおりです。
いくつかの名前を調整しました:
Point1
、n
、l
、およびf
はそれぞれRadialPoint
、count
、factor
、およびtransform
になりました。[ ... ]
を使ったリスト構築をIEnumerable<RadialPoint>
を使ったシーケンス構築に変更することで、RadialPoint list
ではなくseq<RadialPoint>
の戻り値の型を用いました。F# 関数型の代わりに
System.Func
.NET デリゲート型を使用しました。
C# コードでの使用がはるかに使いやすくなります。
.NET