设计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
,我想要的是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#使用compose2
问题! 现在,我必须将所有标准的F# funs
包装到Func
和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#库实现一流的体验?
到目前为止,我不能提出比这两种方法更好的东西:
对于第一种方法,我会做这样的事情:
创建F#项目,将其称为FooBarFs并将其编译到FooBarFs.dll中。
创建另一个F#项目,调用如果FooBarCs并将其编译到FooFar.dll中
我认为命名空间的第二种方法可能会让用户感到困惑,但是你有一个程序集。
问题:这些都不是理想的,也许我缺少某种编译器标志/开关/属性或某种技巧,有更好的方法来做到这一点?
问题:是否有其他人试图实现类似的目标?如果是的话,你是如何做到的?
编辑:澄清,这个问题不仅是关于函数和代表,而是一个C#用户的F#库的总体经验。 这包括C#原生的命名空间,命名约定,习惯用语等。 基本上,C#用户不应该能够检测到该库是在F#中编写的。 反之亦然,F#用户应该像处理C#库一样。
编辑2:
从目前的答案和评论中我可以看出,我的问题缺乏必要的深度,可能主要是由于仅使用了一个F#和C#之间的互操作性问题出现的例子,函数问题是一个值。 我认为这是最明显的例子,所以这让我用它来提问,但同样给人的印象是这是我唯一关心的问题。
让我提供更具体的例子。 我已阅读了最优秀的F#组件设计指南文档(非常感谢@gradbot!)。 如果使用的话,文件中的指导方针确实解决了一些问题,但不是全部。
该文件分为两个主要部分:1)针对F#用户的指南; 和2)针对C#用户的指导原则。 它甚至没有试图假装有可能有一个统一的方法,这正好回应了我的问题:我们可以针对F#,我们可以针对C#,但是针对两者的实用解决方案是什么?
提醒一下,我们的目标是在F#中创建一个库,并且可以从F#和C#语言中惯用地使用这些库。
这里的关键词是惯用的。 问题不在于只能使用不同语言的库的通用互操作性。
现在来看我从F#组件设计指南直接获取的示例。
模块+函数(F#)vs命名空间+类型+函数
F#:请使用名称空间或模块来包含您的类型和模块。 习惯用法是将函数放在模块中,例如:
// library
module Foo
let bar() = ...
let zoo() = ...
// Use from F#
open Foo
bar()
zoo()
C#:使用名称空间,类型和成员作为组件的主要组织结构(与模块相对),使用vanilla .NET API。
这与F#指南不兼容,并且该示例需要重新编写以适合C#用户:
[<AbstractClass; Sealed>]
type Foo =
static member bar() = ...
static member zoo() = ...
通过这样做,我们打破了F#的习惯用法,因为我们不能再使用bar
和zoo
而没有在Foo
加前缀。
使用元组
F#:在适合返回值时使用元组。
C#:避免在香草.NET API中使用元组作为返回值。
异步
F#:在F#API边界处使用异步进行异步编程。
C#:使用.NET异步编程模型(BeginFoo,EndFoo)或作为返回.NET任务(Task)的方法而不是F#Async对象来公开异步操作。
使用Option
F#:考虑使用选项值作为返回类型,而不是引发异常(对于F#表示代码)。
考虑使用TryGetValue模式,而不是在香草.NET API中返回F#选项值(选项),并且更喜欢使用方法重载来将F#选项值作为参数。
歧视的工会
F#:使用区分联合作为类层次结构的替代方法来创建树形结构化数据
C#:没有具体的指导方针,但歧视联盟的概念对C#而言是陌生的,
咖喱功能
F#:咖喱的功能是F#
C#:不要在vanilla .NET API中使用参数的currying。
检查空值
F#:这不是F#的惯用语,
C#:考虑检查vanilla .NET API边界上的空值。
使用F#类型list
, map
, set
等
F#:在F#中使用这些是惯用的,
C#:考虑使用.NET集合接口类型IEnumerable和IDictionary作为参数,并在vanilla .NET API中返回值。 (即不要使用F# list
, map
, set
)
函数类型(明显的)
F#:使用F#函数,因为值是F#的惯用语,显然
C#:在vanilla .NET API中使用.NET委托类型优先于F#函数类型。
我认为这应该足以证明我的问题的性质。
顺便提一下,指南也有部分答案:
...开发用于vanilla .NET库的高阶方法的通用实现策略是使用F#函数类型编写所有实现,然后使用委托作为实际F#实现上的薄外观创建公共API。
总结。
有一个明确的答案:没有编译器技巧,我错过了。
根据指导文档,似乎首先为F#创作,然后为.NET创建外观包装是合理的策略。
那么这个问题的实际执行仍然存在:
单独的组件? 要么
不同的命名空间?
如果我的解释是正确的,Tomas建议使用单独的名称空间应该足够了,应该是一个可接受的解决方案。
我想我会同意,因为命名空间的选择是这样的,它不会让.NET / C#用户感到意外或困惑,这意味着它们的命名空间应该看起来像它是它们的主命名空间。 F#用户将不得不承担选择特定于F#的名称空间的负担。 例如:
FSharp.Foo.Bar - >面向F#的库的命名空间
Foo.Bar - > .NET wrapper的命名空间,习惯于C#
Daniel已经解释了如何定义你编写的F#函数的C#友好版本,所以我会添加一些更高级别的评论。 首先,您应该阅读F#组件设计指南(由gradbot引用)。 这篇文档解释了如何使用F#设计F#和.NET库,它应该回答你的许多问题。
使用F#时,基本上有两种类型的库可以编写:
F#库被设计为仅用于F#,因此它的公共接口是以功能样式编写的(使用F#函数类型,元组,歧义联合等)
.NET库旨在用于任何.NET语言(包括C#和F#),并且通常遵循.NET面向对象的风格。 这意味着你将使用方法(有时候是扩展方法或静态方法,但大多数代码应该写在OO设计中)将大部分功能公开为类。
在你的问题中,你问如何将函数组合作为一个.NET库公开,但我认为像你的compose
这样的函数是从.NET库角度来看太低级别的概念。 你可以将它们暴露为使用Func
和Action
,但这可能不是你如何设计一个普通的.NET库(也许你会使用Builder模式或类似的东西)。
在某些情况下(即设计与.NET库风格不太吻合的数值库时),设计一个在单个库中混合使用F#和.NET风格的库是非常有意义的。 做到这一点的最佳方式是使用普通的F#(或普通的.NET)API,然后为其他风格的自然使用提供包装。 包装可以在一个单独的名称空间(如MyLibrary.FSharp
和MyLibrary
)。
在你的例子中,你可以将F#实现留在MyLibrary.FSharp
,然后在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
是有意义的。 这是否消除了您的担忧? 我认为你提出的解决方案过于复杂。 你可以用F#编写你的整个库,并从F#和C#中使用它(我一直这么做)。
另外,在互操作性方面,F#比C#更灵活,所以当这是一个问题时,通常最好遵循传统的.NET风格。
编辑
我认为,只有当它们互补或F#功能不可用于C#时(例如依赖于F#特定元数据的内联函数),才能保证在单独的命名空间中生成两个公共接口所需的工作。
顺便提一下你的观点:
模块+ let
绑定和无构造函数的类型+静态成员在C#中显示完全一样,所以如果可以,请使用模块。 您可以使用CompiledNameAttribute
为成员提供C#友好的名称。
我可能是错的,但我的猜测是组件指南是在System.Tuple
被添加到框架之前编写的。 (在早期版本中,F#定义了它自己的元组类型。)因为在普通类型的公共接口中使用Tuple
变得更容易被接受。
这就是我认为你已经用C#做事情的地方,因为F#在Task
中表现良好,但C#在Async
表现不佳。 您可以在内部使用async,然后在从公共方法返回之前调用Async.StartAsTask
。
在开发从C#使用的API时,拥抱null
可能是最大的缺点。 在过去,我尝试了各种技巧以避免在内部F#代码中考虑null,但最终最好使用[<AllowNullLiteral>]
标记具有公共构造函数的类型,并检查args是否为null。 在这方面它不比C#差。
被区分的联合通常被编译为类层次结构,但在C#中总是具有相对友好的表示。 我会说,用[<AllowNullLiteral>]
标记并使用它们。
咖喱函数产生不应该使用的函数值。
我发现接受null比依赖它被公共接口捕获并在内部忽略它更好。 因人而异。
在内部使用list
/ map
/ set
很有意义。 它们都可以通过IEnumerable<_>
的公共接口公开。 另外, seq
, dict
和Seq.readonly
通常很有用。
见#6。
您采取的策略取决于您的库的类型和大小,但根据我的经验,在F#和C#之间找到最佳位置需要较少的工作(长期而言),而不是创建单独的API。
虽然它可能会是一个矫枉过正的问题,但您可以考虑使用Mono.Cecil编写应用程序(它在邮件列表上具有很棒的支持),可以自动执行IL级别的转换。 例如,您使用F#风格的公共API在F#中实现您的程序集,然后该工具将生成一个C# - 友好的包装器。
例如,在F#中,显然使用option<'T>
( None
,具体而言),而不是像C#中那样使用null
。 为这个场景编写一个包装器生成器应该相当简单:包装器方法将调用原始方法:如果它的返回值是Some x
,则返回x
,否则返回null
。
当T
是一个值类型时,您将需要处理该情况,即不可为空; 你将不得不将wrapper方法的返回值封装到Nullable<T>
,这会让它有点痛苦。
再次,我相当确定它会在您的方案中写出这样的工具,也许除了定期研究这种库(可以从F#和C#两者无缝使用)之外。 无论如何,我认为这将是一个有趣的实验,我甚至可能会在某个时候进行探索。
链接地址: http://www.djcxy.com/p/80553.html上一篇: Best approach for designing F# libraries for use from both F# and C#