116

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#funsFuncandにラップする必要があります。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# ユーザーを対象とし、2 つ目は C# ユーザーを対象としています。
  2. アセンブリは 1 つですが、名前空間が異なります。1 つは F# ユーザー用、もう 1 つは C# ユーザー用です。

最初のアプローチでは、次のようにします。

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

    • ライブラリは純粋に F# ユーザーを対象とします。
    • .fsi ファイルから不要なものをすべて非表示にします。
  2. 別の 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からそのまま引用した例を次に示し ます。

  1. モジュール + 関数 (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ます。

  2. タプルの使用

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

    • C#: バニラ .NET API で戻り値としてタプルを使用しないようにします。

  3. 非同期

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

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

  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 を使用することを検討してください。(つまり、F# 、 を使用しないlistmapsetでください)

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

4

4 に答える 4

41

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

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

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

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

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

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

この例では、F#実装をそのままMyLibrary.FSharpにして、名前空間に.NET(C#対応)ラッパー(Danielが投稿したコードと同様)をMyLibraryクラスの静的メソッドとして追加できます。しかし、繰り返しになりますが、.NETライブラリには、関数の合成よりも具体的なAPIが含まれている可能性があります。

于 2012-04-11T19:59:31.650 に答える
19

関数値(部分的に適用された関数など) をFuncorでラップするだけ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# ではうまく機能しますTaskが、C# ではAsync. 内部で async を使用Async.StartAsTaskしてから、パブリック メソッドから戻る前に呼び出すことができます。

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

  5. 判別共用体は通常、クラス階層にコンパイルされますが、C# では常に比較的分かりやすい表現になっています。私は、それらをマークして使用すると言い[<AllowNullLiteral>]ます。

  6. カリー化された関数は関数のを生成しますが、これは使用すべきではありません。

  7. パブリック インターフェイスでキャッチされることに依存して内部的に無視するよりも、null を採用する方がよいことがわかりました。YMMV。

  8. list//を内部的に使用することは非常に理にかなっていますmapsetそれらはすべて、パブリック インターフェイスを介して として公開できますIEnumerable<_>。また、seqdict、およびSeq.readonlyも頻繁に役立ちます。

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

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

于 2012-04-11T16:54:40.653 に答える
2

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

たとえば、F#では、C# で likeを使用する代わりに、明らかにoption<'T>( 、具体的には) を使用します。このシナリオのラッパー ジェネレーターを作成するのは非常に簡単です。ラッパー メソッドは元のメソッドを呼び出します。戻り値が の場合は return 、そうでない場合は returnです。NonenullSome xxnull

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

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

于 2012-04-17T22:58:58.543 に答える