我正在尝试在 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
andAction
中,如下所示:
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# 项目,调用 if FooBarCs 并将其编译为 FooFar.dll
- 在源代码级别重用第一个 F# 项目。
- 创建隐藏该项目所有内容的 .fsi 文件。
- 创建以 C# 方式公开库的 .fsi 文件,使用 C# 习惯用法作为名称、命名空间等。
- 创建委托给核心库的包装器,在必要时进行转换。
我认为使用命名空间的第二种方法可能会让用户感到困惑,但是你有一个程序集。
问题:这些都不理想,也许我缺少某种编译器标志/开关/属性或某种技巧,有更好的方法吗?
问题:有没有其他人试图实现类似的东西,如果是这样,你是怎么做到的?
编辑:澄清一下,问题不仅在于函数和委托,还在于 C# 用户使用 F# 库的整体体验。这包括 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#:对于 vanilla .NET API,请务必使用命名空间、类型和成员作为组件(相对于模块)的主要组织结构。
这与 F# 指南不兼容,需要重新编写示例以适应 C# 用户:
[<AbstractClass; Sealed>] type Foo = static member bar() = ... static member zoo() = ...
但是,通过这样做,我们打破了 F# 的惯用用法,因为我们不能再使用
bar
并且zoo
没有前缀Foo
.
元组的使用
F#:在适合返回值时使用元组。
C#:避免在 vanilla .NET API 中使用元组作为返回值。
异步
F#:在 F# API 边界处使用 Async 进行异步编程。
C#:使用 .NET 异步编程模型(BeginFoo、EndFoo)或作为返回 .NET 任务的方法(Task)公开异步操作,而不是作为 F# Async 对象。
用于
Option
F#:考虑对返回类型使用选项值,而不是引发异常(对于面向 F# 的代码)。
考虑使用 TryGetValue 模式而不是在 vanilla .NET API 中返回 F# 选项值(选项),并且更喜欢方法重载而不是将 F# 选项值作为参数。
受歧视的工会
F#:使用可区分联合作为类层次结构的替代方法来创建树结构数据
C#:对此没有具体的指导方针,但区分联合的概念对 C# 来说是陌生的
咖喱函数
F#:柯里化函数是 F# 的惯用语
C#:不要在 vanilla .NET API 中使用参数柯里化。
检查空值
F#:这不是 F# 的惯用语
C#:考虑检查 vanilla .NET API 边界上的空值。
使用 F# 类型
list
,map
,set
, 等F#:在 F# 中使用这些是惯用的
C#:考虑将 .NET 集合接口类型 IEnumerable 和 IDictionary 用于原始 .NET API 中的参数和返回值。(即不要使用 F#
list
,map
,set
)
函数类型(显而易见的)
F#:使用 F# 函数作为值是 F# 的惯用语,显然
C#:在 vanilla .NET API 中使用 .NET 委托类型而不是 F# 函数类型。
我认为这些应该足以证明我的问题的性质。
顺便说一句,指南也有部分答案:
...在为 vanilla .NET 库开发高阶方法时,一种常见的实现策略是使用 F# 函数类型编写所有实现,然后使用委托创建公共 API,作为实际 F# 实现的薄外观。
总结一下。
有一个明确的答案:没有我错过的编译器技巧。
根据指南文档,似乎首先为 F# 创作,然后为 .NET 创建外观包装器是一种合理的策略。
那么问题仍然是关于这个的实际实施:
单独的组件?或者
不同的命名空间?
如果我的解释是正确的,Tomas 建议使用单独的命名空间就足够了,并且应该是一个可以接受的解决方案。
我想我会同意这一点,因为命名空间的选择不会让 .NET/C# 用户感到惊讶或困惑,这意味着他们的命名空间应该看起来像是他们的主要命名空间。F# 用户将不得不承担选择 F# 特定命名空间的负担。例如:
FSharp.Foo.Bar -> 面向库的 F# 命名空间
Foo.Bar -> .NET 包装器的命名空间,C# 的惯用语