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);
}
しかし、F#からのcompose2
の使用に問題があります!次のように、すべての標準F#funs
をFunc
と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つのアプローチよりも良いものを思い付くことができませんでした:
最初のアプローチでは、次のようなことをします。
F#プロジェクトを作成し、FooBarFsと呼び、FooBarFs.dllにコンパイルします。
別のF#プロジェクトを作成し、if FooBarCsを呼び出してFooFar.dllにコンパイルします
名前空間を使用する2番目のアプローチはユーザーを混乱させる可能性がありますが、1つのアセンブリがあります。
質問:これらのどれも理想的ではありません。おそらく、コンパイラのフラグ/スイッチ/属性または何らかのトリックが欠落しており、より良い方法がありますこれを行う方法?
質問:他の誰かが同様の何かを達成しようとしましたが、そうであればどのようにしましたか?
編集:明確にするために、質問は関数とデリゲートだけでなく、F#ライブラリを使用するC#ユーザーの全体的なエクスペリエンスに関するものです。これには、C#固有の名前空間、命名規則、イディオムなどが含まれます。基本的に、C#ユーザーは、ライブラリがF#で作成されたことを検出できないはずです。逆に、F#ユーザーはC#ライブラリを扱うように感じる必要があります。
編集2:
これまでの回答とコメントから、おそらくF#とC#の相互運用性の問題である関数の値の問題を1つだけ使用したために、質問に必要な深さが欠けていることがわかります。これは最も明白な例だと思うので、これを使用して質問をするようになりましたが、同じトークンで、これが私が懸念している唯一の問題であるという印象を与えました。
より具体的な例を挙げましょう。最も優れた F#Component Design Guidelines ドキュメントを読んでいます(これについて@gradbotに感謝します!)。文書内のガイドラインが使用されている場合、すべてではなくいくつかの問題に対処しています。
このドキュメントは2つの主要部分に分かれています。1)F#ユーザーをターゲットにするためのガイドライン。 2)C#ユーザーをターゲットにするためのガイドライン。 F#をターゲットにでき、C#をターゲットにできますが、両方をターゲットにするための実用的な解決策は何ですか?
思い出してほしいのは、F#で作成されたライブラリを使用することであり、F#言語とC#言語の両方からidiomaticallyを使用できることです。
ここのキーワードはidiomaticです。問題は、異なる言語のライブラリを使用するだけの一般的な相互運用性ではありません。
F#コンポーネント設計ガイドライン から直接取り上げる例に移ります。
モジュール+関数(F#)vs名前空間+タイプ+関数
F#:名前空間またはモジュールを使用して、タイプとモジュールを含めます。慣用的な使用法は、モジュールに関数を配置することです、例:
// library
module Foo
let bar() = ...
let Zoo() = ...
// Use from F#
open Foo
bar()
Zoo()
C#:Vanilla .NET APIの場合、(モジュールではなく)コンポーネントの主要な組織構造として名前空間、型、およびメンバーを使用します。
これはF#ガイドラインと互換性がないため、C#ユーザーに合わせて例を書き換える必要があります。
[<AbstractClass; Sealed>]
type Foo =
static member bar() = ...
static member Zoo() = ...
ただし、そうすることで、bar
を前に付けないとZoo
とFoo
を使用できなくなるため、F#の慣用的な使用をやめます。
タプルの使用
F#:戻り値に適切な場合はタプルを使用します。
C#:Vanilla .NET APIで戻り値としてタプルを使用しないでください。
非同期
F#:F#API境界で非同期プログラミングに非同期を使用します。
C#:.NET非同期プログラミングモデル(BeginFoo、EndFoo)を使用して、またはF#非同期オブジェクトとしてではなく、.NETタスクを返すメソッドとして(Task)非同期操作を公開します。
Option
の使用
F#:例外を発生させるのではなく、戻り値の型にオプション値を使用することを検討してください(F#に面したコードの場合)。
Vanilla .NET APIでF#オプション値(オプション)を返す代わりにTryGetValueパターンの使用を検討し、引数としてF#オプション値を取得するよりもメソッドのオーバーロードを優先します。
差別された組合
F#:ツリー構造のデータを作成するために、クラス階層の代替として差別化されたユニオンを使用してください
C#:これに関する特定のガイドラインはありませんが、差別化された共用体の概念はC#にとって異質です
カリー化された機能
F#:カリー化された関数はF#のイディオムです
C#:Vanilla .NET APIでパラメーターのカリー化を使用しないでください。
Null値の確認
F#:これはF#の慣用ではありません
C#:Vanilla .NET APIの境界でnull値を確認することを検討してください。
F#型list
、map
、set
などの使用
F#:F#でこれらを使用するのが慣用的です
C#:Vanilla .NET APIのパラメーターと戻り値に.NETコレクションインターフェイスタイプIEnumerableおよびIDictionaryを使用することを検討してください。 (F#list
、map
、set
を使用しないでください)
関数型(明らかなもの)
F#:値としてのF#関数の使用はF#の慣用的であり、好意的に
C#:Vanilla .NET APIのF#関数型よりも.NETデリゲート型を優先して使用してください。
これらは私の質問の本質を示すのに十分なはずだと思います。
ちなみに、ガイドラインには部分的な答えもあります。
... Vanilla .NETライブラリの高次メソッドを開発する際の一般的な実装戦略は、F#関数型を使用してすべての実装を作成し、実際のF#実装の上に薄いファサードとしてデリゲートを使用してパブリックAPIを作成することです。
要約するために。
1つの明確な答えがあります:私が見逃したコンパイラのトリックはありません。
ガイドラインのドキュメントによると、最初にF#のオーサリングを行い、次に.NETのファサードラッパーを作成するのが合理的な戦略であるようです。
その後、これの実用的な実装に関する疑問が残ります。
アセンブリを分離しますか?または
異なる名前空間?
私の解釈が正しい場合、Tomasは、個別の名前空間を使用するだけで十分であり、許容できる解決策であることを提案します。
名前空間の選択が.NET/C#ユーザーを驚かせたり混乱させたりしないことを考えると、私はそれに同意すると思います。 F#ユーザーは、F#固有の名前空間を選択する負担を負う必要があります。例えば:
FSharp.Foo.Bar-> F#向きライブラリの名前空間
Foo.Bar-> .NETラッパーの名前空間、C#の慣用的
ダニエルは、あなたが書いたF#関数のC#フレンドリーバージョンを定義する方法を既に説明しているので、より高いレベルのコメントを追加します。まず、 F#コンポーネント設計ガイドライン (gradbotによって既に参照されています)を読む必要があります。これは、F#を使用してF#および.NETライブラリを設計する方法を説明するドキュメントであり、多くの質問に答えるはずです。
F#を使用する場合、基本的に2種類のライブラリを作成できます。
F#libraryは、F#からonlyのみを使用するように設計されているため、パブリックインターフェイスは関数で記述されていますスタイル(F#関数型、タプル、識別された共用体などを使用)
。NETライブラリーは、any.NET言語(C#およびF#を含む)から使用するように設計されています。通常、.NETオブジェクト指向スタイルに従います。これは、ほとんどの機能をメソッドを持つクラスとして公開することを意味します(拡張メソッドや静的メソッドもありますが、ほとんどのコードはOOデザイン)で記述する必要があります)。
あなたの質問では、関数構成を.NETライブラリとして公開する方法を尋ねていますが、compose
のような関数は.NETライブラリの観点からは低すぎる概念であると思います。それらをFunc
およびAction
で動作するメソッドとして公開できますが、そもそも通常の.NETライブラリを設計する方法ではないでしょう(おそらくBuilderパターンを使用するでしょう)代わりに、またはそのようなもの)。
場合によっては(つまり、.NETライブラリスタイルにあまり適合しない数値ライブラリを設計する場合)、両方を組み合わせたライブラリを設計するのが理にかなっていますF#および。NETスタイルの単一ライブラリー。これを行う最良の方法は、通常のF#(または通常の.NET)APIを使用し、他のスタイルで自然に使用するためのラッパーを提供することです。ラッパーは、別のネームスペース(MyLibrary.FSharp
やMyLibrary
など)に配置できます。
あなたの例では、F#実装をMyLibrary.FSharp
に残し、.NET(C#に優しい)ラッパー(ダニエルが投稿したコードに類似)をMyLibrary
名前空間にクラスの静的メソッドとして追加できます。 。ただし、ここでも.NETライブラリには、関数の構成よりも特定のAPIが含まれている可能性があります。
関数values(部分的に適用された関数など)をFunc
またはAction
でラップするだけで、残りは自動的に変換されます。例えば:
type A(arg) =
member x.Invoke(f: Func<_,_>) = f.Invoke(arg)
let a = A(1)
a.Invoke(fun i -> i + 1)
したがって、必要に応じてFunc
/Action
を使用することは理にかなっています。これで心配はなくなりましたか?提案されたソリューションは複雑すぎると思います。ライブラリ全体をF#で記述し、F#andC#から簡単に使用できます(私は常にそれを行います)。
また、F#は相互運用性の点でC#よりも柔軟性が高いため、これが懸念される場合は通常、従来の.NETスタイルに従うのが最善です。
編集
2つのパブリックインターフェイスを別々の名前空間に作成するために必要な作業は、それらが補完的であるか、F#機能がC#から使用できない場合(F#固有のメタデータに依存するインライン関数など)にのみ保証されます。
ポイントを順番に取る:
モジュール+ let
バインディングとコンストラクタなしの型+静的メンバーはC#ではまったく同じように見えるため、可能であればモジュールを使用してください。 CompiledNameAttribute
を使用して、メンバーにC#にわかりやすい名前を付けることができます。
私は間違っているかもしれませんが、コンポーネントガイドラインはSystem.Tuple
がフレームワークに追加される前に書かれたと思います。 (以前のバージョンでは、F#は独自のTuple型を定義していました。)それ以来、些細な型のパブリックインターフェイスでTuple
を使用することがより受け入れられるようになりました。
F#はTask
でうまく動作しますが、C#はAsync
でうまく動作しないので、これはあなたがC#のやり方で何かをしたと思うところです。内部で非同期を使用してから、パブリックメソッドから戻る前に Async.StartAsTask
を呼び出すことができます。
null
の採用は、C#から使用するAPIを開発する際の唯一の最大の欠点です。以前は、内部F#コードでnullを考慮しないようにあらゆる種類のトリックを試しましたが、最終的には、[<AllowNullLiteral>]
を使用してパブリックコンストラクターで型をマークし、nullの引数をチェックするのが最善でした。この点でC#よりも悪くはありません。
差別化された共用体は一般にクラス階層にコンパイルされますが、C#では常に比較的フレンドリーな表現を持っています。私は言う、[<AllowNullLiteral>]
でそれらをマークし、それらを使用します。
カリー化された関数は関数valuesを生成しますが、使用しないでください。
パブリックインターフェイスでキャッチされ、内部的に無視するよりも、nullを採用する方が良いことがわかりました。 YMMV。
内部でlist
/map
/set
を使用するのは非常に理にかなっています。これらはすべて、IEnumerable<_>
としてパブリックインターフェイスを介して公開できます。また、 seq
、 dict
、および Seq.readonly
が頻繁に役立ちます。
#6を参照してください。
どの戦略を取るかは、ライブラリの種類とサイズによって異なりますが、私の経験では、F#とC#の間のスイートスポットを見つけるのに必要な作業は、長期的には個別のAPIを作成するよりも少なくて済みます。
おそらくやり過ぎかもしれませんが、ILレベルでの変換を自動化する Mono.Cecil (メーリングリストで素晴らしいサポートがあります)を使用してアプリケーションを作成することを検討できます。たとえば、F#スタイルのパブリックAPIを使用してF#でアセンブリを実装すると、ツールはその上にC#に優しいラッパーを生成します。
たとえば、F#では、C#のようにNone
を使用する代わりに、明らかにoption<'T>
(具体的にはnull
)を使用します。このシナリオのラッパージェネレーターの作成は非常に簡単です。ラッパーメソッドは元のメソッドを呼び出します。戻り値がSome x
の場合、x
を返し、そうでない場合はnull
を返します。
T
が値型、つまり、null不可の場合を処理する必要があります。ラッパーメソッドの戻り値をNullable<T>
にラップする必要があるため、少し苦痛になります。
繰り返しますが、このようなライブラリを(F#とC#の両方からシームレスに使用できるように)定期的に作業する場合を除き、シナリオでこのようなツールを作成しても成果を上げることは間違いありません。いずれにせよ、それは面白い実験になると思います。いつか探検するかもしれません。
ドラフトF#コンポーネント設計ガイドライン(2010年8月)
概要このドキュメントでは、F#コンポーネントの設計とコーディングに関連する問題の一部を取り上げます。特に、以下をカバーします。
- 任意の.NET言語から使用する「Vanilla」.NETライブラリを設計するためのガイドライン。
- F#からF#へのライブラリとF#実装コードのガイドライン。
- F#実装コードのコーディング規則に関する提案