我正在尝试在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#包装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#用户,第二个针对C#用户。
- 一个程序集但名称空间不同:一个用于F#用户,另一个用于C#用户。
对于第一种方法,我将执行以下操作:
创建一个F#项目,将其命名为FooBarFs并将其编译为FooBarFs.dll。
- 仅将库定位为F#用户。
- 隐藏.fsi文件中不需要的所有内容。
创建另一个F#项目,调用FooBarCs并将其编译为FooFar.dll
- 在源代码级别重用第一个F#项目。
- 创建.fsi文件,该文件将隐藏该项目中的所有内容。
- 创建.fsi文件,以C#方式公开库,并使用C#惯用语来表示名称,名称空间等。
- 创建委派给核心库的包装器,并在必要时进行转换。
我认为使用名称空间的第二种方法可能会使用户感到困惑,但是您只有一个程序集。
问题:这些都不是理想的,也许我缺少某种编译器标志/开关/属性或某种技巧,还有更好的方法吗?
问题是:是否有其他人试图实现类似目标?如果是,您是如何做到的?
编辑:澄清一下,这个问题不仅涉及函数和委托,而且还涉及具有F#库的C#用户的整体经验。这包括C#固有的名称空间,命名约定,惯用语等。基本上,C#用户不应检测到该库是用F#编写的。反之亦然,F#用户应该感觉像在处理C#库。
编辑2:
从到目前为止的答案和评论中可以看出,我的问题缺乏必要的深度,这可能主要是由于仅使用了一个示例,其中出现了F#和C#之间的互操作性问题,即函数值的问题。我认为这是最明显的示例,因此这使我用它来提出问题,但出于同样的原因,给人的印象是这是我唯一关心的问题。
让我提供更具体的例子。我已经阅读了最出色的《 F#组件设计指南》 文档(非常感谢@gradbot!)。如果使用本文档中的准则,则确实可以解决某些问题,但不能解决所有问题。
该文档分为两个主要部分:1)针对F#用户的准则;和2)定位C#用户的准则。甚至没有人假装有可能采用统一的方法,这正好回应了我的问题:我们可以针对F#,我们可以针对C#,但是同时针对这两种方法的实际解决方案是什么?
提醒一下,目标是要有一个用F#编写的库,并且可以从F#和C#语言中惯用该库。
这里的关键字是惯用语。问题不是一般的互操作性,在这种互操作性下,有可能使用不同语言的库。
现在来看示例,我直接取自《 F#组件设计指南》。
模块+功能(F#)与命名空间+类型+功能
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
。
元组的使用
F#:在适合返回值时,请使用元组。
C#:避免将元组用作原始.NET API中的返回值。
异步
F#:请在F#API边界使用Async进行异步编程。
C#:请使用.NET异步编程模型(BeginFoo,EndFoo)或作为返回.NET任务的方法(任务)而不是F#异步对象公开异步操作。
用于
Option
F#:考虑将选项值用于返回类型,而不是引发异常(对于面向F#的代码)。
考虑使用TryGetValue模式,而不是在原始.NET API中返回F#选项值(选项),并且比将F#选项值作为参数更喜欢方法重载。
歧视工会
F#:请使用区分联合作为类层次结构的替代方法,以创建树结构数据
C#:对此没有特定的指导原则,但是区别工会的概念对C#来说是陌生的
咖喱函数
F#:咖喱函数是F#惯用的
C#:请勿在普通的.NET API中使用参数传递。
检查空值
F#:这不是F#的惯用语
C#:考虑检查香草.NET API边界上的空值。
F#类型的使用
list
,map
,set
等F#:在F#中使用它们是惯用的
C#:考虑将.NET集合接口类型IEnumerable和IDictionary用于原始.NET API中的参数和返回值。(即不使用F# ,
list
,map
set
)
函数类型(显而易见的一种)
F#:使用F#函数作为值对于F#来说是惯用的
C#:在.NET API中,请优先使用.NET委托类型,而不要使用F#函数类型。
我认为这些应该足以证明我的问题的性质。
顺便说一句,指南也有部分答案:
在为香草.NET库开发高阶方法时,常见的实现策略是使用F#函数类型编写所有实现,然后使用委托作为实际F#实现之上的薄外观来创建公共API。
总结一下。
有一个明确的答案:没有我错过的编译器技巧。
根据指南文档,似乎首先为F#创作,然后为.NET创建外观包装是一种合理的策略。
关于此的实际实施,问题仍然存在:
单独的组件?要么
不同的名称空间?
如果我的解释正确,Tomas建议使用单独的名称空间就足够了,应该是可以接受的解决方案。
考虑到命名空间的选择不会使.NET / C#用户感到惊讶或困惑,我想我同意这一点,这意味着它们的命名空间可能看起来像是它们的主要命名空间。F#用户将不得不承担选择F#特定名称空间的负担。例如:
FSharp.Foo.Bar-> F#面向库的名称空间
Foo.Bar-> .NET包装的名称空间,对于C#是惯用的