F# でライブラリを設計しようとしています。このライブラリは、F# と C# の両方から使用できるようにする必要があります。
そして、これは私が少し立ち往生しているところです。F# フレンドリーにすることも、C# フレンドリーにすることもできますが、問題は、両方にとってフレンドリーにする方法です。
ここに例があります。F# に次の関数があるとします。
let compose (f: 'T -> 'TResult) (a : 'TResult -> unit) = f >> a
これは F# から完全に使用できます。
let useComposeInFsharp() =
let composite = compose (fun item -> item.ToString) (fun item -> printfn "%A" item)
composite "foo"
composite "bar"
C# では、compose
関数には次のシグネチャがあります。
FSharpFunc<T, Unit> compose<T, TResult>(FSharpFunc<T, TResult> f, FSharpFunc<TResult, Unit> a);
FSharpFunc
しかしもちろん、署名は必要Func
ありませんAction
。代わりに、次のようにします。
Action<T> compose2<T, TResult>(Func<T, TResult> f, Action<TResult> a);
これを実現するために、次のcompose2
ような関数を作成できます。
let compose2 (f: Func<'T, 'TResult>) (a : Action<'TResult> ) =
new Action<'T>(f.Invoke >> a.Invoke)
これで、C# で完全に使用できるようになりました。
void UseCompose2FromCs()
{
compose2((string s) => s.ToUpper(), Console.WriteLine);
}
しかしcompose2
、F# からの使用に問題があります。ここで、次のように、すべての標準 F#funs
をFunc
andにラップする必要があります。Action
let useCompose2InFsharp() =
let f = Func<_,_>(fun item -> item.ToString())
let a = Action<_>(fun item -> printfn "%A" item)
let composite2 = compose2 f a
composite2.Invoke "foo"
composite2.Invoke "bar"
質問: F# と C# の両方のユーザーにとって、F# で記述されたライブラリで最高のエクスペリエンスを実現するにはどうすればよいでしょうか?
これまでのところ、次の 2 つのアプローチより優れたものは思いつきませんでした。
- 2 つの別個のアセンブリ: 1 つは F# ユーザーを対象とし、2 つ目は C# ユーザーを対象としています。
- アセンブリは 1 つですが、名前空間が異なります。1 つは F# ユーザー用、もう 1 つは C# ユーザー用です。
最初のアプローチでは、次のようにします。
F# プロジェクトを作成し、それを FooBarFs と呼び、FooBarFs.dll にコンパイルします。
- ライブラリは純粋に F# ユーザーを対象とします。
- .fsi ファイルから不要なものをすべて非表示にします。
別の F# プロジェクトを作成し、if FooBarCs を呼び出して FooFar.dll にコンパイルします。
- ソース レベルで最初の F# プロジェクトを再利用します。
- そのプロジェクトからすべてを隠す .fsi ファイルを作成します。
- 名前、名前空間などに C# のイディオムを使用して、C# の方法でライブラリを公開する .fsi ファイルを作成します。
- 必要に応じて変換を行い、コア ライブラリに委譲するラッパーを作成します。
名前空間を使用した 2 番目のアプローチは、ユーザーを混乱させる可能性があると思いますが、アセンブリは 1 つです。
質問:これらはどれも理想的ではありません。おそらく、ある種のコンパイラ フラグ/スイッチ/属性またはある種のトリックが不足しており、これを行うためのより良い方法はありますか?
質問: 他の誰かが同様のことを達成しようとしたことがありますか?もしそうなら、どのようにそれを行いましたか?
編集: 明確にするために、質問は関数とデリゲートだけでなく、F# ライブラリを使用する C# ユーザーの全体的な経験についてです。これには、C# 固有の名前空間、命名規則、イディオムなどが含まれます。基本的に、C# ユーザーは、ライブラリが F# で作成されたことを検出できないはずです。逆に、F# ユーザーは C# ライブラリを扱いたくなるはずです。
編集2:
これまでの回答とコメントから、私の質問には必要な深さが欠けていることがわかります。これは、おそらく主に、F# と C# の間の相互運用性の問題、関数値の問題が発生する 1 つの例のみを使用したためです。これが最もわかりやすい例だと思いますので、これを使って質問しましたが、同様に、これが私が関心を持っている唯一の問題であるという印象を与えました。
もっと具体的な例を挙げましょう。私は最も優れた F# コンポーネント デザイン ガイドライン ドキュメントを読みました (これについては @gradbot に感謝します!)。このドキュメントのガイドラインを使用すると、一部の問題は解決されますが、すべてではありません。
このドキュメントは、次の 2 つの主要部分に分かれています。1) F# ユーザーを対象とするためのガイドライン。2) C# ユーザーをターゲットにするためのガイドライン。F# をターゲットにすることも、C# をターゲットにすることもできますが、両方をターゲットにするための実際的な解決策は何ですか?
念のために言っておきますが、目標は F# で作成されたライブラリを作成することであり、F# と C# の両方の言語から慣用的に使用できます。
ここでのキーワードは慣用的です。問題は、さまざまな言語でライブラリを使用できるという一般的な相互運用性ではありません。
F# Component Design Guidelinesからそのまま引用した例を次に示し ます。
モジュール + 関数 (F#) vs 名前空間 + 型 + 関数
F#: 名前空間またはモジュールを使用して、型とモジュールを含めてください。慣用的な使用法は、関数をモジュールに配置することです。たとえば、次のようになります。
// library module Foo let bar() = ... let zoo() = ... // Use from F# open Foo bar() zoo()
C#: バニラ .NET API の場合は、(モジュールではなく) コンポーネントの主要な組織構造として、名前空間、型、およびメンバーを使用してください。
これは F# ガイドラインと互換性がなく、C# ユーザーに合わせて例を書き直す必要があります。
[<AbstractClass; Sealed>] type Foo = static member bar() = ... static member zoo() = ...
ただし、そうすることで、慣用的な使用法を F# から
bar
切り離すzoo
ことができFoo
ます。
タプルの使用
F#: 戻り値に適切な場合はタプルを使用してください。
C#: バニラ .NET API で戻り値としてタプルを使用しないようにします。
非同期
F#: F# API 境界での非同期プログラミングには Async を使用してください。
C#: F# Async オブジェクトとしてではなく、.NET 非同期プログラミング モデル (BeginFoo、EndFoo) を使用するか、.NET タスク (Task) を返すメソッドとして非同期操作を公開します。
の使用
Option
F#: 例外を発生させる代わりに、戻り値の型にオプション値を使用することを検討してください (F# 向けのコードの場合)。
通常の .NET API で F# オプション値 (オプション) を返す代わりに TryGetValue パターンを使用することを検討し、F# オプション値を引数として受け取るよりもメソッドのオーバーロードを優先してください。
差別された組合
F#: ツリー構造のデータを作成するためのクラス階層の代わりに判別共用体を使用してください
C#:これに関する具体的なガイドラインはありませんが、判別共用体の概念は C# とは無関係です
カリー化された関数
F#:カリー化された関数は F# の慣用句です
C#: バニラ .NET API でパラメーターのカリー化を使用しないでください。
null 値のチェック
F#:これは F# の慣用句ではありません
C#: バニラ .NET API 境界で null 値をチェックすることを検討してください。
F# 型の使用
list
、map
、set
などF#: F#でこれらを使用するのは慣用的です
C#: 標準の .NET API でパラメーターと戻り値に .NET コレクション インターフェイス型 IEnumerable と IDictionary を使用することを検討してください。(つまり、F# 、、 を使用しない
list
map
set
でください)
関数型 (明らかなもの)
F#: F# 関数を値として使用することは、明らかに F# の慣用句です。
C#: バニラ .NET API では、F# 関数型よりも .NET デリゲート型を使用してください。
私の質問の性質を示すには、これらで十分だと思います。
ちなみに、ガイドラインにも部分的な回答があります。
...バニラ .NET ライブラリの高次メソッドを開発する際の一般的な実装戦略は、F# 関数型を使用してすべての実装を作成し、実際の F# 実装の上に薄いファサードとしてデリゲートを使用してパブリック API を作成することです。
要約すると。
明確な答えが 1 つあります。私が見逃したコンパイラのトリックはありません。
ガイドライン ドキュメントによると、最初に F# 用に作成してから、.NET 用のファサード ラッパーを作成するのが合理的な戦略のようです。
次に、これの実際の実装に関して疑問が残ります。
アセンブリを分離しますか?また
名前空間が違う?
私の解釈が正しければ、別の名前空間を使用することで十分であり、受け入れられる解決策であると Tomas は示唆しています。
名前空間の選択が .NET/C# ユーザーを驚かせたり混乱させたりしないようなものであることを考えると、私はそれに同意すると思います。F# ユーザーは、F# 固有の名前空間を選択する負担を負う必要があります。例えば:
FSharp.Foo.Bar -> ライブラリに面する F# の名前空間
Foo.Bar -> .NET ラッパーの名前空間、C# の慣用句