deviseF#库以供F#和C#使用的最佳方法

我正试图在F#中devise一个库。 图书馆应该友好的使用F#和C#

这就是我被卡住的地方。 我可以让F#友好,或者我可以让C#友好,但问题是如何使它对两者友好。

这是一个例子。 想象一下,我在F#中有以下function:

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); 

为了达到这个目的,我可以像这样创buildcompose2函数:

 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#使用compose2问题! 现在我必须将所有标准的F# funs包装到FuncAction ,像这样:

 let useCompose2InFsharp() = let f = Func<_,_>(fun item -> item.ToString()) let a = Action<_>(fun item -> printfn "%A" item) let composite2 = compose2 fa composite2.Invoke "foo" composite2.Invoke "bar" 

问题:我们如何才能为用F#和C#用户编写的F#库实现一stream的体验?

到目前为止,我不能拿出比这两种方法更好的东西:

  1. 两个独立的程序集:一个针对F#用户,另一个针对C#用户。
  2. 一个程序集,但名称空间不同:一个用于F#用户,另一个用于C#用户。

对于第一种方法,我会做这样的事情:

  1. 创buildF#项目,将其称为FooBarFs并将其编译到FooBarFs.dll中。

    • 将该库纯粹定位到F#用户。
    • 从.fsi文件中隐藏不必要的东西。
  2. 创build另一个F#项目,调用如果FooBarCs并将其编译到FooFar.dll

    • 重用源代码级别的第一个F#项目。
    • 创build隐藏该项目的所有内容的.fsi文件。
    • 创build.fsi文件,以C#方式公开库,使用C#成语来表示名称,命名空间等。
    • 创build委托给核心库的包装,在必要时进行转换。

我认为用命名空间的第二种方法可能会让用户感到困惑,但是你有一个程序集。

问题:这些都不是理想的,也许我缺less某种编译器标志/开关/属性或某种技巧,有更好的方法呢?

问题是:有没有其他人试图做出类似的事情,如果是的话,你是怎么做到的?

编辑:澄清,这个问题不仅是关于函数和代表,而是一个C#用户与F#库的总体经验。 这包括C#原生的命名空间,命名约定,习惯用语等。 基本上,C#用户不应该能够检测到该库是在F#中编写的。 反之亦然,F#用户应该感觉像处理一个C#库。


编辑2:

从目前的答案和评论中我可以看出,我的问题缺乏必要的深度,也许主要是因为只使用了一个F#和C#之间互操作性问题的例子,函数问题就是一个值。 我认为这是最明显的例子,所以这让我用它来提出这个问题,但同样给人的印象是这是我唯一关心的问题。

让我提供更具体的例子。 我已阅读了最优秀的F#组件devise指南文档(非常感谢@gradbot!)。 如果使用的话,文件中的指导方针确实解决了一些问题,但不是全部。

该文件分为两个主要部分:1)针对F#用户的指导原则; 和2)针对C#用户的指南。 它甚至没有试图假装可以有一个统一的方法,这正好回应了我的问题:我们可以针对F#,我们可以针对C#,但是针对两者的实际解决scheme是什么?

提醒一下,我们的目标是在F#中创build一个库,并且可以从F#和C#两种语言中惯用地使用这个库。

这里的关键词是惯用的 。 问题不在于只能使用不同语言的库的通用互操作性。

下面是我从F#组件devise指南中直接获取的示例。

  1. 模块+函数(F#)与命名空间+types+函数

    • F#:使用命名空间或模块来包含你的types和模块。 习惯用法是将函数放在模块中,例如:

       // library module Foo let bar() = ... let zoo() = ... // Use from F# open Foo bar() zoo() 
    • C#:使用名称空间,types和成员作为组件的主要组织结构(与模块相对),使用vanilla .NET API。

      这与F#指南不兼容,为了适合C#用户,需要重新编写示例:

       [<AbstractClass; Sealed>] type Foo = static member bar() = ... static member zoo() = ... 

      通过这样做,我们打破了F#的习惯用法,因为我们不能再使用barzoo而没有在Foo加前缀。

  2. 使用元组

    • F#:适当时使用元组返回值。

    • C#:避免在香草.NET API中使用元组作为返回值。

  3. asynchronous

    • F#:在F#API边界使用Async进行asynchronous编程。

    • C#:使用.NETasynchronous编程模型(BeginFoo,EndFoo)或作为返回.NET任务(Task)的方法而不是F#Async对象来公开asynchronous操作。

  4. 使用Option

    • F#:考虑使用返回types的选项值,而不是引发exception(对于F#代码)。

    • 考虑使用TryGetValue模式,而不是在香草.NET API中返回F#选项值(选项),而更喜欢方法重载以F#选项值作为参数。

  5. 歧视的工会

    • F#:使用区分的联合作为类层次结构的替代方法来创build树形结构化的数据

    • C#: 对此没有具体的指导,但歧义联盟的概念是C#

  6. 咖喱function

    • F#: 咖喱的function是F#

    • C#:不要在香草.NET API中使用参数的currying。

  7. 检查空值

    • F#: 这不是F#的惯用语法

    • C#:考虑检查香草.NET API边界上的空值。

  8. 使用F#typeslistmapset

    • F#: 在F#中使用这些是惯用的,

    • C#:考虑使用.NET集合接口typesIEnumerable和IDictionary作为参数,并在vanilla .NET API中返回值。 ( 即不要使用F# listmapset

  9. 函数types(明显的)

    • F#: 使用F#函数作为值是F#的惯用语,显然

    • C#:使用.NET委托types优先于在香草.NET API中的F#函数types。

我认为这应该足以说明我的质询的性质。

顺便说一下,指南也有部分答案:

当为香草.NET库开发更高阶的方法时,一个通用的实现策略是使用F#函数types创作所有实现,然后使用代表作为实际F#实现之上的薄外观来创build公共API。

总结。

有一个确定的答案: 没有编译器的技巧,我错过了

根据指导文档,似乎先编写F#,然后创build一个.NET的外观包装是合理的策略。

那么这个问题的实际执行仍然是一个问题:

  • 单独的组件? 要么

  • 不同的命名空间?

如果我的解释是正确的,托马斯build议使用单独的名称空间应该是足够的,应该是一个可接受的解决scheme。

我想我会同意,因为命名空间的select是这样的,它不会使.NET / C#用户感到惊讶或困惑,这意味着它们的命名空间应该看起来像它们的主命名空间。 F#用户将不得不承担select特定于F#的命名空间的负担。 例如:

  • FSharp.Foo.Bar – >面向F#的库的命名空间

  • Foo.Bar – > .NET包装器的命名空间,习惯于C#

Daniel已经解释了如何定义你编写的F#函数的C#友好版本,所以我会添加一些更高级的注释。 首先,您应该阅读F#组件devise指南 (由gradbot引用)。 这是一个文档,解释如何使用F#deviseF#和.NET库,它应该回答你的许多问题。

使用F#时,基本上有两种types的库可以编写:

  • F#库被devise为仅用于F#,因此它的公共接口是以函数式(使用F#函数types,元组,歧义联合等)编写的。

  • .NET库旨在用于任何 .NET语言(包括C#和F#),并且通常遵循.NET面向对象的风格。 这意味着你将大部分的function作为具有方法的类(有时是扩展方法或者静态方法,但是大多数代码应该被写在OOdevise中)公开。

在你的问题中,你在问如何将函数组合作为一个.NET库来公开,但是我认为像你的compose这样的函数是从.NET库angular度来看太低级的概念。 你可以将它们暴露为使用FuncAction ,但是这可能不是如何devise一个普通的.NET库(也许你会使用Builder模式或类似的东西)。

在某些情况下(例如,devise与.NET库风格不太吻合的数值库时),devise一个在单个库中混合使用F#.NET风格的库是非常有意义的。 最好的方法是使用普通的F#(或普通的.NET)API,然后在其他风格中提供自然使用的包装。 包装可以在一个单独的名称空间(如MyLibrary.FSharpMyLibrary )。

在你的例子中,你可以在MyLibrary.FSharp保留F#实现,然后在MyLibrary命名空间中添加.NET(C# – 友好)包装器(类似于Daniel发布的代码),作为某些类的静态方法。 但是,.NET库可能会比函数组合更具体的API。

你只需要用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是有意义的。 这是否消除你的顾虑? 我认为你提出的解决scheme过于复杂。 你可以用F#编写你的整个库,并且可以从F# C#中使用它(我一直这么做)。

另外,在互操作性方面,F#比C#更灵活,所以当这是一个问题时,通常最好遵循传统的.NET风格。

编辑

我认为,在两个独立的命名空间中创build两个公共接口所需的工作仅在互补或F#function无法从C#使用(例如依赖于F#特定元数据的内联函数)时才是有保证的。

顺便提一下你的观点:

  1. 模块+ let绑定和无构造函数types+静态成员在C#中显示完全相同,所以如果可以,请使用模块。 你可以使用CompiledNameAttribute给成员C#友好的名字。

  2. 我可能是错的,但我的猜测是组件指南是在System.Tuple被添加到框架之前编写的。 (在早期版本中,F#定义了它自己的元组types。)因为在公共接口中使用Tuple变得更容易被接受。

  3. 这是我认为你有做事情的C#方式,因为F#与Task发挥良好,但C#不能很好地与Async 。 您可以在内部使用asynchronous,然后在从公共方法返回之前调用Async.StartAsTask

  4. 在开发从C#使用的API时,拥抱null可能是最大的缺点。 在过去,我尝试了各种各样的技巧,以避免在内部F#代码中考虑null,但最后最好使用[<AllowNullLiteral>]标记具有公共构造函数的types,并检查args是否为null。 在这方面它不比C#差。

  5. 被区分的联合通常被编译为类层次结构,但在C#中总是有相对友好的表示。 我会说,用[<AllowNullLiteral>]标记它们并使用它们。

  6. 咖喱函数产生函数 ,不应该使用。

  7. 我发现接受空值比依靠它被公众接口抓住而忽略内部要好。 因人而异。

  8. 在内部使用list / map / set是非常有意义的。 它们都可以通过公共接口暴露为IEnumerable<_> 。 另外, seqdictSeq.readonly常常是有用的。

  9. 见#6。

您采取的策略取决于您的库的types和大小,但根据我的经验,在F#和C#之间寻找最佳位置需要较less的工作(长期而言),而不是创build单独的API。

F#组件devise指南草案(2010年8月)

概述本文讨论与F#组件devise和编码相关的一些问题。 具体包括:

  • 从任何.NET语言devise“vanilla”.NET库的准则。
  • F#到F#库和F#实现代码的准则。
  • 关于F#实现代码编码约定的build议

虽然这可能是一个矫枉过正的问题,但是你可以考虑使用Mono.Cecil来编写一个应用程序(它在邮件列表上有非常好的支持),它将自动完成IL级别的转换。 例如,你用F#实现你的程序集,使用F#风格的公共API,那么这个工具会生成一个C#友好的包装器。

例如,在F#中,显然使用option<'T>None ,具体),而不是像在C#中使用null 。 为这个场景编写一个包装器生成器应该相当简单:包装器方法将调用原始方法:如果返回值是Some x ,则返回x ,否则返回null

T是一个值types时,你将需要处理这个情况,即不可空; 你将不得不把wrapper方法的返回值换成Nullable<T> ,这样做有点痛苦。

再次,我相当确定,在您的场景中编写这样一个工具还是值得的,也许除了定期开发这样的库(从F#和C#都可以无缝使用)之外。 无论如何,我认为这将是一个有趣的实验,有时我甚至可能会进行探索。