设计F#库以供F#和C#使用的最佳方法

Phi*_* P. 104 c# f#

我正在尝试用F#设计一个库.该库应该对F#和C#都很友好.

这就是我被困住的地方.我可以使它F#友好,或者我可以使它C#友好,但问题是如何使它友好的两者.

这是一个例子.想象一下,我在F#中有以下功能:

let compose (f: 'T -> 'TResult) (a : 'TResult -> unit) = f >> a
Run Code Online (Sandbox Code Playgroud)

这完全可以从F#中使用:

let useComposeInFsharp() =
    let composite = compose (fun item -> item.ToString) (fun item -> printfn "%A" item)
    composite "foo"
    composite "bar"
Run Code Online (Sandbox Code Playgroud)

在C#中,该compose函数具有以下签名:

FSharpFunc<T, Unit> compose<T, TResult>(FSharpFunc<T, TResult> f, FSharpFunc<TResult, Unit> a);
Run Code Online (Sandbox Code Playgroud)

但当然我不想FSharpFunc签名,我想要的是Func,Action而是像这样:

Action<T> compose2<T, TResult>(Func<T, TResult> f, Action<TResult> a);
Run Code Online (Sandbox Code Playgroud)

为此,我可以创建这样的compose2函数:

let compose2 (f: Func<'T, 'TResult>) (a : Action<'TResult> ) = 
    new Action<'T>(f.Invoke >> a.Invoke)
Run Code Online (Sandbox Code Playgroud)

现在这在C#中完全可用:

void UseCompose2FromCs()
{
    compose2((string s) => s.ToUpper(), Console.WriteLine);
}
Run Code Online (Sandbox Code Playgroud)

但是现在我们在使用compose2F#时遇到了问题!现在我必须将所有标准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"
Run Code Online (Sandbox Code Playgroud)

问题是:对于F#和C#用户,我们如何为F#编写的库实现一流的体验?

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

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

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

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

    • 将库完全定位到F#用户.
    • 隐藏.fsi文件中不必要的一切.
  2. 创建另一个F#项目,调用FooBarCs并将其编译为FooFar.dll

    • 在源级别重用第一个F#项目.
    • 创建.fsi文件,隐藏该项目中的所有内容.
    • 创建.fsi文件,以C#方式公开库,使用C#idioms作为名称,名称空间等.
    • 创建委托给核心库的包装器,在必要时进行转换.

我认为使用命名空间的第二种方法可能会让用户感到困惑,但是你有一个程序集.

问题:这些都不是理想的,也许我缺少某种编译器标志/开关/ attribte或某种技巧,还有更好的方法吗?

问题是:有没有其他人试图实现类似的东西,如果是这样,你是怎么做到的?

编辑:澄清一下,问题不仅在于函数和代理,还在于带有F#库的C#用户的整体体验.这包括C#原生的命名空间,命名约定,习语等.基本上,C#用户不应该能够检测到库是用F#编写的.反之亦然,F#用户应该感觉像是在处理C#库.


编辑2:

到目前为止,我可以从答案和评论中看出,我的问题缺乏必要的深度,可能主要是因为只使用了F#和C#之间的互操作性问题,功能问题的问题.我认为这是最明显的例子,所以这导致我用它来提出问题,但同样令人印象的是这是我唯一关心的问题.

让我提供更具体的例子.我已经阅读了最优秀的 F#组件设计指南 文档(非常感谢@gradbot!).如果使用该文件中的指南,则可以解决一些问题,但不是全部问题.

该文件分为两个主要部分:1)针对F#用户的指南; 2)针对C#用户的指南.它甚至没有尝试假装有可能采用统一的方法,这恰好回应了我的问题:我们可以针对F#,我们可以针对C#,但是针对两者的实际解决方案是什么?

要提醒的是,我们的目标是在F#编写的一个图书馆,并可以使用惯用来自F#和C#语言.

这里的关键字是惯用的.问题不在于可以使用不同语言的库的一般互操作性.

现在我将直接从F#组件设计指南中获取示例 .

  1. 模块+函数(F#)vs命名空间+类型+函数

    • F#:使用命名空间或模块来包含您的类型和模块.惯用的用法是将函数放在模块中,例如:

      // library
      module Foo
      let bar() = ...
      let zoo() = ...
      
      
      // Use from F#
      open Foo
      bar()
      zoo()
      
      Run Code Online (Sandbox Code Playgroud)
    • C#:对于vanilla .NET API,请使用名称空间,类型和成员作为组件的主要组织结构(而不​​是模块).

      这与F#指南不兼容,需要重新编写示例以适合C#用户:

      [<AbstractClass; Sealed>]
      type Foo =
          static member bar() = ...
          static member zoo() = ...
      
      Run Code Online (Sandbox Code Playgroud)

      通过这样做,我们打破了F#的习惯用法,因为我们不能再使用它barzoo不用前缀Foo.

  2. 使用元组

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

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

  3. 异步

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

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

  4. 用于 Option

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

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

  5. 受歧视的工会

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

    • C#:没有具体的指导方针,但歧视联盟的概念对C#来说是陌生的

  6. 咖喱功能

    • F#:curried函数是F#的惯用语

    • C#:不要在vanilla .NET API中使用currying参数.

  7. 检查空值

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

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

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

    • F#:在F#中使用它们是惯用的

    • C#:考虑使用.NET集合接口类型IEnumerable和IDictionary作为参数并在vanilla .NET API中返回值.(即不使用F# ,list,mapset)

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

Tom*_*cek 37

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

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

  • F#库被设计用来从F#,所以它的公共接口是用一个实用的风格(使用F#函数类型,元组,识别联合等)

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

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

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

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

  • F# 组件设计指南现在托管在 [此处](https://docs.microsoft.com/en-us/dotnet/fsharp/style-guide/component-design-guidelines) (2认同)

Dan*_*iel 17

您只需要用或包装函数(部分应用的函数等),其余的都会自动转换.例如:FuncAction

type A(arg) =
  member x.Invoke(f: Func<_,_>) = f.Invoke(arg)

let a = A(1)
a.Invoke(fun i -> i + 1)
Run Code Online (Sandbox Code Playgroud)

因此,在适用的情况下使用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在内部使用async,然后在从公共方法返回之前调用.

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

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

  6. Curried函数生成函数,不应使用它们.

  7. 我发现拥抱null比依赖它被公共接口捕获并在内部忽略它更好.因人而异.

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

  9. 见#6.

您采取的策略取决于库的类型和大小,但根据我的经验,在F#和C#之间找到最佳位置需要较少的工作 - 从长远来看 - 而不是创建单独的API.