116

我正在尝试在 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);
}

但是现在我们在使用compose2F# 时遇到了问题!现在我必须将所有标准 F# 包装funsFuncandAction中,如下所示:

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# 编写的库提供一流的体验?

到目前为止,我想不出比这两种方法更好的方法:

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

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

  1. 创建一个 F# 项目,将其命名为 FooBarFs 并将其编译为 FooBarFs.dll。

    • 将库完全定位到 F# 用户。
    • 从 .fsi 文件中隐藏所有不必要的内容。
  2. 创建另一个 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# 组件设计指南

  1. 模块+函数(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.

  2. 元组的使用

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

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

  3. 异步

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

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

  4. 用于Option

    • F#:考虑对返回类型使用选项值,而不是引发异常(对于面向 F# 的代码)。

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

  5. 受歧视的工会

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

    • C#:对此没有具体的指导方针,但区分联合的概念对 C# 来说是陌生的

  6. 咖喱函数

    • F#:柯里化函数是 F# 的惯用语

    • C#:不要在 vanilla .NET API 中使用参数柯里化。

  7. 检查空值

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

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

  8. 使用 F# 类型list, map, set, 等

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

    • C#:考虑将 .NET 集合接口类型 IEnumerable 和 IDictionary 用于原始 .NET API 中的参数和返回值。(即不要使用 F# list, map,set

  9. 函数类型(显而易见的)

    • 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# 的惯用语

4

4 回答 4

41

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

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

  • F# 库被设计为只能从 F# 中使用,因此它的公共接口以函数式样式编写(使用 F# 函数类型、元组、可区分联合等)

  • .NET 库旨在用于任何.NET 语言(包括 C# 和 F#),它通常遵循 .NET 面向对象的风格。这意味着您将把大部分功能公开为带有方法的类(有时是扩展方法或静态方法,但大部分代码应该在 OO 设计中编写)。

在您的问题中,您问的是如何将函数组合公开为 .NET 库,但我认为compose从 .NET 库的角度来看,像您这样的函数是太低级的概念。您可以将它们公开为使用Funcand的方法Action,但这可能不是您最初设计普通 .NET 库的方式(也许您会使用 Builder 模式或类似的东西)。

在某些情况下(即在设计与 .NET 库样式不太匹配的数值库时),设计一个在单个库中混合F#.NET样式的库是很有意义的。做到这一点的最好方法是拥有普通的 F#(或普通的 .NET)API,然后提供包装器以在其他样式中自然使用。包装器可以位于单独的命名空间中(如MyLibrary.FSharpand MyLibrary)。

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

于 2012-04-11T19:59:31.650 回答
19

您只需用or包装函数(部分应用的函数等),其余的将自动转换。例如: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 样式。

编辑

我认为,在单独的命名空间中创建两个公共接口所需的工作只有在它们互补或不能从 C# 中使用 F# 功能(例如内联函数,它依赖于 F# 特定的元数据)时才得到保证。

依次考虑你的观点:

  1. 模块 +let绑定和无构造函数类型 + 静态成员在 C# 中看起来完全一样,所以尽可能使用模块。您可以使用CompiledNameAttribute为成员提供 C# 友好的名称。

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

  3. 这就是我认为您以 C# 方式做事的地方,因为 F#TaskAsync. 您可以在内部使用异步,然后Async.StartAsTask在从公共方法返回之前调用。

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

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

  6. 柯里化函数产生函数,不应使用。

  7. 我发现拥抱 null 比依赖它在公共接口中被捕获并在内部忽略它更好。YMMV。

  8. 在内部使用list//很有意义mapset它们都可以通过公共接口公开为IEnumerable<_>. 此外,seqdictSeq.readonly经常有用。

  9. 见#6。

采取哪种策略取决于库的类型和大小,但根据我的经验,从长远来看,找到 F# 和 C# 之间的最佳点比创建单独的 API 需要更少的工作。

于 2012-04-11T16:54:40.653 回答
2

尽管这可能是一种矫枉过正,但您可以考虑使用Mono.Cecil编写一个应用程序(它在邮件列表中提供了非常棒的支持),它将在 IL 级别自动转换。例如,您使用 F# 样式的公共 API 在 F# 中实现您的程序集,然后该工具将在其上生成一个 C# 友好的包装器。

例如,在 F# 中,您显然会使用option<'T>( None,特别是) 而不是null在 C# 中使用 like。为这种情况编写一个包装器生成器应该相当容易:包装器方法将调用原始方法:如果它的返回值是Some x,则返回x,否则返回null

您将需要处理何时T是值类型的情况,即不可为空;您必须将包装方法的返回值包装到Nullable<T>中,这有点痛苦。

同样,我很确定在您的场景中编写这样的工具会有所回报,除非您将定期使用这样的库(可从 F# 和 C# 无缝使用)。无论如何,我认为这将是一个有趣的实验,我什至可能会在某个时候进行探索。

于 2012-04-17T22:58:58.543 回答
2

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

概述 本文档着眼于与 F# 组件设计和编码相关的一些问题。特别是,它涵盖:

  • 设计用于任何 .NET 语言的“vanilla”.NET 库的指南。
  • F# 到 F# 库和 F# 实现代码的指南。
  • F#实现代码的编码约定建议
于 2012-04-12T16:42:21.077 回答