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#からの使用に問題があります!今、私はこのようfuns
にFunc
、すべての標準F#を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#ユーザーを対象とし、もう1つはC#ユーザーを対象とします。
- 1つのアセンブリが異なる名前空間:1つはF#ユーザー用、2つ目はC#ユーザー用。
最初のアプローチでは、私はこのようなことをします:
F#プロジェクトを作成し、FooBarFsと呼び、FooBarFs.dllにコンパイルします。
- ライブラリを純粋にF#ユーザーにターゲティングします。
- .fsiファイルから不要なものをすべて隠します。
別のF#プロジェクトを作成し、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#コンポーネントの設計ガイドラインから直接取った例について説明し ます。
モジュール+関数(F#)と名前空間+タイプ+関数
F#:名前空間またはモジュールを使用して、タイプとモジュールを含めます。慣用的な使い方は、関数をモジュールに配置することです。例:
// library module Foo let bar() = ... let zoo() = ... // Use from F# open Foo bar() zoo()
C#:(モジュールではなく)コンポーネントの主要な組織構造として、名前空間、型、およびメンバーを使用してください。
これはF#ガイドラインと互換性がなく、C#ユーザーに合わせて例を書き直す必要があります。
[<AbstractClass; Sealed>] type Foo = static member bar() = ... static member zoo() = ...
しかし、そうすることで、F#から慣用的な使用をやめます。これは、使用できなくなり
bar
、zoo
接頭辞としてが付いていないためFoo
です。
タプルの使用
F#:戻り値に適切な場合はタプルを使用してください。
C#:バニラ.NET APIでは戻り値としてタプルを使用しないでください。
非同期
F#:F#API境界での非同期プログラミングには非同期を使用してください。
C#:F#非同期オブジェクトとしてではなく、.NET非同期プログラミングモデル(BeginFoo、EndFoo)、または.NETタスクを返すメソッド(タスク)のいずれかを使用して、非同期操作を公開します。
の使用
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を使用することを検討してください。(つまり
list
、F#、、を使用しない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#の慣用句