F#とC#の両方から使用するF#ライブラリを設計するための最良のアプローチ


113

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署名で、私が欲しいのであるFuncAction、このように、代わりに:

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#からの使用に問題があります!今、私はこのようfunsFunc、すべての標準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つのアプローチより良いものを思いつくことができませんでした:

  1. 2つの別個のアセンブリ:1つはF#ユーザーを対象とし、もう1つはC#ユーザーを対象とします。
  2. 1つのアセンブリが異なる名前空間:1つはF#ユーザー用、2つ目はC#ユーザー用。

最初のアプローチでは、私はこのようなことをします:

  1. F#プロジェクトを作成し、FooBarFsと呼び、FooBarFs.dllにコンパイルします。

    • ライブラリを純粋にF#ユーザーにターゲティングします。
    • .fsiファイルから不要なものをすべて隠します。
  2. 別の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#コンポーネントの設計ガイドラインから直接取った例について説明し ます。

  1. モジュール+関数(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#から慣用的な使用をやめます。これは、使用できなくなりbarzoo接頭辞としてが付いていないためFooです。

  2. タプルの使用

    • F#:戻り値に適切な場合はタプルを使用してください。

    • C#:バニラ.NET APIでは戻り値としてタプルを使用しないでください。

  3. 非同期

    • F#:F#API境界での非同期プログラミングには非同期を使用してください。

    • C#:F#非同期オブジェクトとしてではなく、.NET非同期プログラミングモデル(BeginFoo、EndFoo)、または.NETタスクを返すメソッド(タスク)のいずれかを使用して、非同期操作を公開します。

  4. の使用 Option

    • F#:例外を発生させるのではなく、戻り値の型にオプション値を使用することを検討してください(F#向けのコードの場合)。

    • バニラ.NET APIでF#オプション値(オプション)を返すのではなく、TryGetValueパターンの使用を検討してください。また、引数としてF#オプション値を取得するよりもメソッドのオーバーロードを優先してください。

  5. 差別された組合

    • F#:ツリー構造のデータを作成するためのクラス階層の代わりに、区別された共用体を使用してください

    • C#:これに関する特定のガイドラインはありませんが、差別化された労働組合の概念は C#には関係ありません

  6. カレー機能

    • F#:カリー化された関数はF#の慣用的です

    • C#:バニラ.NET APIではパラメーターのカリー化を使用しないでください。

  7. null値の確認

    • F#:これは F#では慣用的ではありません

    • C#:バニラ.NET API境界でnull値をチェックすることを検討してください。

  8. F#のタイプの使用listmapset、など

    • F#:これらをF#で使用するのは慣用的です

    • C#:バニラ.NET APIのパラメーターと戻り値に.NETコレクションインターフェイスタイプIEnumerableおよびIDictionaryを使用することを検討してください。(つまりlist、F#、、を使用しないmapでくださいset

  9. 関数タイプ(明白なもの)

    • 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#の慣用句



ファーストクラスの関数を渡す以外に、懸念事項は何ですか?F#は、他の言語からのシームレスなエクスペリエンスを提供するために、すでに長い道のりを進んでいます。
Daniel

Re:あなたの編集、F#で作成されたライブラリがC#から非標準に見えると思われる理由については、まだはっきりしません。ほとんどの場合、F#はC#機能のスーパーセットを提供します。ライブラリを自然に感じるようにすることは、ほとんどが慣習の問題です。これは、使用している言語に関係なく当てはまります。つまり、F#は、これを行うために必要なすべてのツールを提供します。
Daniel

正直なところ、コードサンプルを含めても、かなり自由回答式の質問をしています。「F#ライブラリを使用したC#ユーザーの全体的なエクスペリエンス」について質問しますが、与える唯一の例は命令型言語では実際にはあまりサポートされていないものです。関数をファーストクラスの市民として扱うことは、関数型プログラミングの中心的な考え方です。これは、ほとんどの命令型言語にうまく対応できません。
オノリオカテナッチ2012

2
@KomradeP .: EDIT 2に関して、FSharpxは相互運用性をより見栄えよくするいくつかの手段を提供します。この優れたブログ投稿でヒントを見つけることができます。
パッド

回答:


40

ダニエルは、あなたが書いたF#関数のC#対応バージョンを定義する方法をすでに説明したので、より高レベルのコメントをいくつか追加します。まず、F#コンポーネントの設計ガイドライン(gradbotによって既に参照されています)をお読みください。これは、F#を使用してF#および.NETライブラリを設計する方法を説明するドキュメントであり、多くの質問に答えるものです。

F#を使用する場合、基本的に2種類のライブラリを記述できます。

  • F#ライブラリF#からのみ使用するように設計されているため、そのパブリックインターフェイスは関数形式で記述されています(F#関数タイプ、タプル、識別された共用体などを使用)。

  • .NETライブラリは、任意の .NET言語(C#およびF#を含む)から使用できるように設計されており、通常は.NETオブジェクト指向のスタイルに従います。これは、ほとんどの機能をメソッド付きのクラスとして公開することを意味します(拡張メソッドや静的メソッドが含まれることもありますが、ほとんどの場合、コードはOO設計で記述する必要があります)。

あなたの質問では、関数の構成を.NETライブラリとして公開する方法を尋ねていますが、あなたのような関数composeは.NETライブラリの観点からは低レベルの概念だと思います。それらをFuncActionで動作するメソッドとして公開できますが、通常の.NETライブラリを最初に設計する方法とはおそらく異なります(おそらくBuilderパターンを使用するなど)。

場合によっては(つまり、.NETライブラリスタイルにうまく適合しない数値ライブラリを設計する場合)、F#スタイルと.NETスタイルの両方を1つのライブラリに混在させるライブラリを設計することは理にかなっています。これを行う最善の方法は、通常のF#(または通常の.NET)APIを用意し、他のスタイルで自然に使用できるラッパーを提供することです。ラッパーは別の名前空間に置くことができます(MyLibrary.FSharpおよびなどMyLibrary)。

あなたの例では、F#実装をそのままにして、MyLibrary.FSharp.NET(C#フレンドリー)ラッパー(Danielが投稿したコードと同様)をMyLibraryクラスの静的メソッドとして名前空間に追加できます。ただし、ここでも、.NETライブラリには、関数の構成よりも具体的なAPIがおそらくあります。


1
F#コンポーネントのデザインガイドラインがここでホストされます
Jan Schiefer、

19

関数(部分的に適用される関数など)を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#から痛みのない、それを使用することができますし、 C#(私はそれをすべての時間を行います)。

また、F#は相互運用性の点でC#よりも柔軟性が高いため、これが問題になる場合は、一般的に従来の.NETスタイルに従うのが最善です。

編集

2つのパブリックインターフェイスを別々の名前空間で作成するために必要な作業は、補完的であるか、F#機能がC#から使用できない場合(F#固有のメタデータに依存するインライン関数など)にのみ保証されます。

順番にポイントを取る:

  1. モジュール+ letバインディングとコンストラクターなしの型+静的メンバーはC#でもまったく同じように表示されるため、可能であればモジュールを使用してください。を使用CompiledNameAttributeして、メンバーにC#フレンドリーな名前を付けることができます。

  2. 私は間違っているかもしれませんが、私の推測では、コンポーネントガイドラインはSystem.Tupleフレームワークに追加される前に作成されたと思います。(以前のバージョンでは、F#は独自のタプル型を定義していました。)それ以降Tuple、自明な型のパブリックインターフェイスでの使用が受け入れられるようになりました。

  3. これは、F#はうまく機能しますが、C#はとうまくTask機能しないため、C#の方法で行う必要があると思う場所ですAsync。内部でasyncを使用してAsync.StartAsTaskから、パブリックメソッドから戻る前に呼び出すことができます。

  4. 抱擁は、nullC#から利用するためのAPIを開発する際に、単一の最大の欠点かもしれません。以前は、内部F#コードでnullを考慮しないようにあらゆる種類のトリックを試しましたが、最終的には、パブリックコンストラクターで型をマークし[<AllowNullLiteral>]、argsにnullがあるかどうかをチェックするのが最善でした。この点でC#よりも悪くはありません。

  5. 差別化された共用体は、通常、クラス階層にコンパイルされますが、常にC#では比較的親しみやすい表現になります。私は、それらに印を付け[<AllowNullLiteral>]て使用すると言います。

  6. カリー化された関数は、使用すべきではない関数値を生成します。

  7. nullを受け入れる方が、パブリックインターフェースでキャッチされ、内部で無視されるよりも良いことがわかりました。YMMV。

  8. それは多くの意味で使用することができますlist/ map/ set内部。これらはすべて、パブリックインターフェイスを介して公開できますIEnumerable<_>。また、seqdict、とSeq.readonly頻繁に便利です。

  9. #6を参照してください。

どの戦略を取るかは、ライブラリのタイプとサイズによって異なりますが、私の経験では、F#とC#の間のスイートスポットを見つけることは、個別のAPIを作成するよりも、長い目で見ると少ない作業で済みます。


はい、私は物事を機能させるいくつかの方法があることを見ることができます、私は完全に確信していません。再モジュール。あなたがmodule Foo.Bar.ZooC#を持っている場合、これはclass Foo名前空間になりますFoo.Bar。ただし、のようなネストされたモジュールがある場合module Foo=... module Bar=... module Zoo==、これはFoo内部クラスBarとを含むクラスを生成しZooます。再度IEnumerable、これはマップとセットを操作する必要がある場合には受け入れられないかもしれません。null値を再評価し、AllowNullLiteral-私がF#を好きな理由の1つはnull、C#に飽き飽きしていて、本当にすべてを再び汚したくないからです!
フィリップP.

一般に、相互運用にはネストされたモジュールよりも名前空間を優先します。Re:null、私はあなたに同意します。それを考慮する必要があるのは確かに退行ですが、APIがC#から使用される場合は対処する必要があります。
Daniel

2

多分やり過ぎでしょうが、ILレベルでの変換を自動化するMono.Cecil(メーリングリストですばらしいサポートがあります)を使用してアプリケーションを作成することを検討できます。たとえば、F#スタイルのパブリックAPIを使用してF#でアセンブリを実装すると、ツールはその上にC#対応のラッパーを生成します。

たとえば、F#では、C#のように使用するのではなく、option<'T>None特に)を使用しnullます。このシナリオのラッパージェネレーターの作成はかなり簡単です。ラッパーメソッドは元のメソッドを呼び出します。戻り値がの場合はSome x、次にを返しx、そうでない場合はを返しnullます。

T値型の場合、つまりnull不可の場合に対処する必要があります。ラッパーメソッドの戻り値をにラップする必要Nullable<T>があるため、少し面倒です。

繰り返しますが、このようなツールを(F#とC#の両方からシームレスに使用できるように)定期的に作業する場合を除いて、シナリオでそのようなツールを書くことは報われると確信しています。いずれにしても、興味深い実験になると思います。


面白そう。Mono.Cecil基本的に、F#アセンブリをロードするプログラムを作成し、マッピングが必要なアイテムを見つけ、C#ラッパーで別のアセンブリを出力するという意味ですか?私はこれを正しく理解しましたか?
フィリップP.

@Komrade P .:あなたもそれを行うことができますが、新しいアセンブリのメンバーを指すようにすべての参照を調整する必要があるため、それははるかに複雑です。新しいメンバー(ラッパー)を既存のF#で作成されたアセンブリに追加するだけの場合は、はるかに簡単です。
ShdNx 2012

2

F#コンポーネント設計ガイドラインのドラフト(2010年8月)

概要 このドキュメントでは、F#コンポーネントの設計とコーディングに関連するいくつかの問題について説明します。特に、次の内容について説明します。

  • 任意の.NET言語から使用するための「バニラ」.NETライブラリを設計するためのガイドライン。
  • F#-to-F#ライブラリおよびF#実装コードのガイドライン。
  • F#実装コードのコーディング規約に関する提案
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.