繁体   English   中英

设计用于 F# 和 C# 的 F# 库的最佳方法

[英]Best approach for designing F# libraries for use from both F# and 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 ,我想要的是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);
}

但是现在我们在使用 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 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 文件,该文件隐藏该项目中的所有内容。
    • 创建 .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# 组件设计指南中获取的示例。

  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# 的惯用用法,因为我们不能再使用barzoo而不用Foo前缀。

  2. 元组的使用

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

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

  3. 异步

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

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

  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# 类型listmapset

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

    • C#:考虑将 .NET 集合接口类型 IEnumerable 和 IDictionary 用于 vanilla .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# 惯用的命名空间

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

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

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

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

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

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

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

你只需要用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 风格。

编辑

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

依次拿你的分数:

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

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

  3. 这就是我认为你可以用 C# 方式做事的地方,因为 F# 与Task配合得很好,但 C# 与Async配合得不好。 您可以在内部使用异步,然后在从公共方法返回之前调用Async.StartAsTask

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

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

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

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

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

  9. 见#6。

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

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

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

  • 设计用于任何 .NET 语言的“vanilla”.NET 库的指南。
  • F# 到 F# 库和 F# 实现代码的准则。
  • 关于 F# 实现代码的编码约定的建议

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM