2012-04-11 32 views
92

我想在F#中設計一個庫。該圖書館應友好使用從都F#和C#設計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#的問題!現在,我必須包裝所有標準的F#funsFuncAction,像這樣:

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#兩個F#和C#用戶庫一流的經驗嗎?

到目前爲止,我不能想出什麼比這兩種方法更好:

  1. 兩個獨立的組件:一個針對F#的用戶,第二個爲C#用戶。
  2. 一個程序集,但名稱空間不同:一個用於F#用戶,另一個用於C#用戶。

對於第一種方法,我會做這樣的事情:

  1. 創建F#項目,稱之爲FooBarFs並把它編譯成FooBarFs.dll。

    • 僅將目標庫定位到F#用戶。
    • 從.fsi文件中隱藏不必要的東西。
  2. 創建另一個F#項目,調用if FooBarCs並將其編譯到FooFar中。dll

    • 在源代碼級別重新使用第一個F#項目。
    • 創建隱藏該項目所有內容的.fsi文件。
    • 創建使用C#方式公開庫的.fsi文件,使用C#成語來表示名稱,名稱空間等。
    • 創建委託給核心庫的包裝,在必要時進行轉換。

我認爲與命名空間的第二種方法可以迷惑用戶,但你有一個程序集。

問題:這些都不是理想的,也許我缺少某種編譯器標誌/開關/ attribte 或某種把戲並沒有這樣做的更好的辦法?

問題:有其他人試圖實現類似的東西,如果是的話你是怎麼做到的?

編輯:澄清,這個問題不僅是關於函數和代表,而是一個C#用戶的F#庫的總體經驗。這包括C#原生的命名空間,命名約定,習慣用語等。基本上,C#用戶不應該能夠檢測到該庫是在F#中編寫的。反之亦然,F#用戶應該像處理C#庫一樣。


編輯2:

我可以從答案和註釋中看到,到目前爲止,我的問題缺乏必要的深度, 也許主要是由於只使用一個例子,其中F#之間的互操作性問題, C# 出現了,函數的問題出現了一個值。我認爲這是最明顯的例子,所以這 使我用它來問這個問題,但同樣的道理給我的印象是,這是我所關心的唯一問題 。

讓我提供更具體的例子。我已閱讀了最優秀的 F# Component Design Guidelines 文檔(非常感謝@gradbot!)。如果使用了文檔中的指導原則,則可以解決 中的一些問題,但不是全部。

該文件分爲兩個主要部分:1)針對F#用戶的指導原則;和2)針對C#用戶的 指南。它甚至沒有試圖假裝可以有統一的 方法,這正好迴應了我的問題:我們可以定位F#,我們可以定位C#,但是針對兩者的實用解決方案是什麼?

提醒,目標是在F#中創建一個庫,並且可以使用慣用的從 F#和C#語言。

這裏的關鍵字是慣用的。這個問題不是一般的互操作性,只是可能的 使用不同語言的庫。

現在來看我從 F# Component Design Guidelines直接得到的例子。

  1. 模塊+功能(F#)與命名空間+類型+功能

    • F#:不要使用命名空間或模塊包含您的類型和模塊。 的習慣用法是把功能模塊,例如:

      // library 
      module Foo 
      let bar() = ... 
      let zoo() = ... 
      
      
      // Use from F# 
      open Foo 
      bar() 
      zoo() 
      
    • C#:執行使用名稱空間,類型和成員作爲主要的組織結構爲您 部件(而不是模塊),香草.NET蜜蜂。

      這與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#:不要使用暴露無論是.NET異步編程模型 (BeginFoo,EndFoo)異步操作,或爲返回.NET任務的方法(任務),而不是F#異步 對象。

  4. 使用Option

    • F#:考慮使用選項值返回類型,而不是引發異常(用於F#-facing代碼)。

    • 考慮使用TryGetValue模式,而不是在香草 .NET API中返回F#選項值(選項),並且首選方法重載以將F#選項值作爲參數。

  5. 識別聯合

    • F#:不要使用可識別聯合作爲替代類層次結構用於創建樹結構數據

    • C#:對此沒有具體的指導方針,但歧視聯盟的概念對於C而言是外國的#

  6. 咖喱功能

    • F#:咖喱功能是地道的F#

    • C#:不要使用的香草.NET API的參數鑽營。

  7. 檢查NULL值

    • F#:這不是地道的F#

    • C#:考慮檢查香草.NET API邊界空值。

  8. 使用F#類型listmapset

    • F#:是慣用使用這些在F#

    • C#:使用.NET考慮收集界面類型IEnumerable和IDictionary ,以獲取vanilla .NET API中的參數和返回值。 (即不使用F#listmapset

  9. 函數類型(明顯的一個)

    • F#:使用的F#的功能值是慣用爲F#,有意識地

    • C#:使用.NET委託類型優先於在香草.NET API中的F#函數類型。

我想這些應該足以證明我的問題的性質。

順便說一句,這些準則也有部分答案:

...共同執行戰略發展高階 方法香草.NET庫是使用F#功能類型來編寫所有的實現時,和 然後使用委託創建公共API作爲實際F#實現上的薄外觀。

總結。

有一個確定的答案:沒有編譯器技巧,我錯過了

根據指導文檔,似乎首先編寫F#然後創建一個.NET外觀包裝是合理的策略。

的問題然後保持關於切實落實這一點:

  • 獨立的組件?或

  • 不同的命名空間?

如果我的解釋是正確的,托馬斯認爲,使用不同的命名空間應該 足夠了,而應該是一個可以接受的解決方案。

我想我會用,鑑於命名空間的選擇是這樣的,它 不驚或混淆的.NET/C#用戶,這意味着它們的命名空間 也許應該像它同意是主他們的名字空間。 F#用戶將不得不承擔選擇特定於F#的名稱空間的負擔。 例如:

  • FSharp.Foo.Bar - 面向> F#的命名空間庫

  • Foo.Bar - >命名空間爲.NET包裝,慣用的C#

+8

有點相關http://research.microsoft.com/en-us/um/cambridge/projects/fsharp/manual/fsharp-component-design-guidelines.pdf – gradbot 2012-04-11 16:43:17

+0

除了傳遞一流的功能,什麼是你的擔憂? F#已經在通過其他語言提供無縫體驗方面邁出了很大的步伐。 – Daniel 2012-04-11 17:12:26

+0

回覆:您的編輯,我還不清楚爲什麼您認爲在F#中編寫的圖書館在C#中顯示爲非標準。大多數情況下,F#提供了C#功能的超集。讓圖書館感覺自然很大程度上是一種慣例,無論您使用何種語言,都是如此。所有這一切,F#提供了你需要做的所有工具。 – Daniel 2012-04-11 21:18:57

回答

15

你只需要將的值(部分應用的函數等)與FuncAction包裝在一起,其餘的將自動轉換。例如:

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風格。

EDIT

使在單獨的名稱空間兩個公共接口所需要的工作,我想,僅必要時它們是互補的或F#功能不是從C#可用(如聯的函數,依賴於F# - 特定的元數據)。

以你點依次爲:

  1. + let綁定和構造少型+靜態成員恰好出現在C#一樣的,所以如果你能模塊安裝模塊。你可以使用CompiledNameAttribute給成員C#友好的名字。

  2. 我可能是錯的,但我的猜測是組件指南是在System.Tuple被添加到框架之前編寫的。 (在早期版本中,F#定義了它自己的元組類型。)因爲在公共接口中使用Tuple以使它們變得更加可以接受,所以它們可以用於不重要的類型。

  3. 這是我認爲你已經在做C#方式的事情,因爲F#與Task搭配很好,但C#與Async的搭配不好。您可以在內部使用異步,然後在從公共方法返回之前調用Async.StartAsTask

  4. 當開發從C#使用的API時,擁抱null可能是最大的缺點。在過去,我嘗試了各種技巧來避免在內部F#代碼中考慮null,但最後,最好用公共構造函數標記類型爲[<AllowNullLiteral>],並檢查args爲null。在這方面它不比C#差。

  5. 歧視聯盟通常編譯爲類層次結構,但始終在C#中具有相對友好的表示形式。我會說,用[<AllowNullLiteral>]來標記它們並使用它們。

  6. 咖喱函數產生函數,不應該使用。

  7. 我發現接受null比依靠它被公共接口捕獲並在內部忽略它更好。因人而異。

  8. 在內部使用list/map/set是很有意義的。他們都可以通過公共界面暴露爲IEnumerable<_>。另外,seq,dictSeq.readonly通常是有用的。

  9. 參見#6。

你採取哪種策略取決於你的庫的類型和大小,但是,根據我的經驗,發現F#和C#之間的甜蜜點需要更少的工作,從長遠來看,不是創建單獨的API。

+0

是的,我可以看到有一些方法可以使事情發揮作用,我不完全相信。重新模塊。如果你有'module Foo.Bar.Zoo',那麼在C#中這將是'Foo.Bar'命名空間中的'class Foo'。但是,如果你有像'module Foo = ... module Bar = ... module Zoo =='這樣的嵌套模塊,這將生成內部類爲Bar和Zoo的類Foo。 Re'IEnumerable',當我們真的需要使用地圖和集合時,這可能是不可接受的。 Re'null'值和'AllowNullLiteral' - 我喜歡F#的一個原因是因爲我厭倦了C#中的null,真的不想再次污染它的一切! – 2012-04-13 10:24:18

+0

通常,爲了互操作而傾向於嵌套模塊上的命名空間。 Re:null,我同意你的觀點,這肯定是一個必須考慮的迴歸,但是如果你的API將從C#中使用,你必須處理一些事情。 – Daniel 2012-04-13 14:37:24

33

Daniel已經解釋瞭如何定義您編寫的F#函數的C#友好版本,因此我將添加一些更高級別的註釋。首先,你應該閱讀F# Component Design Guidelines(已經被gradbot引用)。這篇文檔解釋瞭如何使用F#設計F#和.NET庫,它應該回答你的許多問題。

當使用F#,基本上有兩種類型的圖書館,你可以這樣寫:

  • F#庫被設計爲僅從F#中使用,所以它的公共接口是用的功能式(使用F#函數類型,元組,識別聯合等)

  • NET庫被設計成從任何使用.NET語言(包括C#和F#),它通常遵循.NET面向對象的風格。這意味着你將使用方法(有時候是擴展方法或靜態方法,但大多數代碼應該寫在OO設計中)將大部分功能公開爲類。

在你的問題,你問如何公開函數組合爲.NET庫,但我認爲像你compose功能是從視.NET庫點過低層次的概念。你可以將它們公開爲使用FuncAction的方法,但這可能不是你如何設計一個普通的.NET庫(也許你會使用Builder模式或類似的東西)。

在某些情況下(即設計數字圖書館並不真正與.NET庫風格合身時),它使一個良好的感覺來設計混合了兩種F#.NET風格的圖書館單一圖書館。做到這一點的最佳方式是使用普通的F#(或普通的.NET)API,然後爲其他風格的自然使用提供包裝。包裝可以在一個單獨的名稱空間(如MyLibrary.FSharpMyLibrary)。

在你的榜樣,你可以留在MyLibrary.FSharp F#的實現,然後添加.NET(C# - 友好)包裝(類似的代碼,丹尼爾貼)在某些類的MyLibrary命名空間的靜態方法。但是,.NET庫可能會比函數組合更具體的API。

2

Draft F# Component Design Guidelines (August 2010)

概述本文着眼於一些與F#組件設計和編碼問題。特別是,它涵蓋:

  • 設計「香草」.NET庫的指南,可用於任何.NET語言。
  • F#-to-F#庫和F#實現代碼準則。編碼約定的F#的實現代碼
  • 建議
2

雖然它很可能是矯枉過正,你可以考慮寫使用Mono.Cecil的應用程序(它在郵件列表上真棒支持),將自動轉換在IL級別。例如,您使用F#風格的公共API在F#中實現您的程序集,然後該工具將生成一個C# - 友好的包裝器。

例如,在F#中,您顯然會使用option<'T>(特別是None),而不是像C#中那樣使用null。爲這種情況編寫包裝器生成器應該相當簡單:包裝器方法將調用原始方法:如果返回值爲Some x,則返回x,否則返回null

當您需要處理T是一個值類型時,即不可爲空;你將不得不將wrapper方法的返回值換成Nullable<T>,這會讓它有點痛苦。

同樣,我很確定它會在您的場景中編寫這樣一個工具,也許除了定期處理此類庫(可以從F#和C#兩者無縫使用)之外。無論如何,我認爲這將是一個有趣的實驗,我甚至可能會在某個時候進行探索。

+0

聽起來很有趣。將使用'Mono.Cecil'意味着基本上編寫一個程序來加載F#程序集,找到需要映射的項目,並且它們用C#包裝器發出另一個程序集?我有這個權利嗎? – 2012-04-18 11:25:49

+0

@Komrade P .:你也可以做到這一點,但那會更加複雜,因爲你必須調整所有的引用來指向新程序集的成員。如果只是將新成員(包裝器)添加到現有的F#寫入的程序集中,這會更簡單。 – ShdNx 2012-04-18 12:29:21