2012-04-11 53 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