设计F#库以供F#和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);
}

但是现在我们compose2来自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#库实现一流的体验?

到目前为止,我不能提出比这两种方法更好的东西:

  • 两个独立的程序集:一个针对F#用户,另一个针对C#用户。
  • 一个程序集,但名称空间不同:一个用于F#用户,另一个用于C#用户。
  • 对于第一种方法,我会做这样的事情:

  • 创建F#项目,将其称为FooBarFs并将其编译到FooBarFs.dll中。

  • 纯粹将目标库定位到F#用户。
  • 隐藏.fsi文件中不必要的所有内容。
  • 创建另一个F#项目,调用如果FooBarCs并将其编译到FooFar.dll中

  • 重用源代码级别的第一个F#项目。
  • 创建隐藏该项目所有内容的.fsi文件。
  • 创建.fsi文件,以C#方式公开库,使用C#成语来表示名称,命名空间等。
  • 创建委托给核心库的包装,在必要时进行转换。
  • 我认为命名空间的第二种方法可能会让用户感到困惑,但是你有一个程序集。

    问题:这些都不是理想的,也许我缺少某种编译器标志/开关/属性或某种技巧,有更好的方法来做到这一点?

    问题:是否有其他人试图实现类似的目标?如果是的话,你是如何做到的?

    编辑:澄清,这个问题不仅是关于函数和代表,而是一个C#用户的F#库的总体经验。 这包括C#原生的命名空间,命名约定,习惯用语等。 基本上,C#用户不应该能够检测到该库是在F#中编写的。 反之亦然,F#用户应该像处理C#库一样。


    编辑2:

    从目前的答案和评论中我可以看出,我的问题缺乏必要的深度,可能主要是由于仅使用了一个F#和C#之间的互操作性问题出现的例子,函数问题是一个值。 我认为这是最明显的例子,所以这让我用它来提问,但同样给人的印象是这是我唯一关心的问题。

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

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

    提醒一下,我们的目标是在F#中创建一个库,并且可以从F#和C#语言中惯用地使用这些库。

    这里的关键词是惯用的。 问题不在于只能使用不同语言的库的通用互操作性。

    现在来看我从F#组件设计指南直接获取的示例。

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

  • 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加前缀。

  • 使用元组

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

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

  • 异步

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

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

  • 使用Option

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

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

  • 歧视的工会

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

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

  • 咖喱功能

  • F#:咖喱的功能是F#

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

  • 检查空值

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

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

  • 使用F#类型listmapset

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

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

  • 函数类型(明显的)

  • F#:使用F#函数,因为值是F#的惯用语,显然

  • C#:在vanilla .NET API中使用.NET委托类型优先于F#函数类型。

  • 我认为这应该足以证明我的问题的性质。

    顺便提一下,指南也有部分答案:

    ...开发用于vanilla .NET库的高阶方法的通用实现策略是使用F#函数类型编写所有实现,然后使用委托作为实际F#实现上的薄外观创建公共API。

    总结。

    有一个明确的答案:没有编译器技巧,我错过了。

    根据指导文档,似乎首先为F#创作,然后为.NET创建外观包装是合理的策略。

    那么这个问题的实际执行仍然存在:

  • 单独的组件? 要么

  • 不同的命名空间?

  • 如果我的解释是正确的,Tomas建议使用单独的名称空间应该足够了,应该是一个可接受的解决方案。

    我想我会同意,因为命名空间的选择是这样的,它不会让.NET / C#用户感到意外或困惑,这意味着它们的命名空间应该看起来像它是它们的主命名空间。 F#用户将不得不承担选择特定于F#的名称空间的负担。 例如:

  • FSharp.Foo.Bar - >面向F#的库的命名空间

  • Foo.Bar - > .NET wrapper的命名空间,习惯于C#


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

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

  • F#库被设计为仅用于F#,因此它的公共接口是以功能样式编写的(使用F#函数类型,元组,歧义联合等)

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

  • 在你的问题中,你问如何将函数组合作为一个.NET库公开,但我认为像你的compose这样的函数是从.NET库角度来看太低级别的概念。 你可以将它们暴露为使用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#特定元数据的内联函数),才能保证在单独的命名空间中生成两个公共接口所需的工作。

    顺便提一下你的观点:

  • 模块+ let绑定和无构造函数的类型+静态成员在C#中显示完全一样,所以如果可以,请使用模块。 您可以使用CompiledNameAttribute为成员提供C#友好的名称。

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

  • 这就是我认为你已经用C#做事情的地方,因为F#在Task中表现良好,但C#在Async表现不佳。 您可以在内部使用async,然后在从公共方法返回之前调用Async.StartAsTask

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

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

  • 咖喱函数产生不应该使用的函数值。

  • 我发现接受null比依赖它被公共接口捕获并在内部忽略它更好。 因人而异。

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

  • 见#6。

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


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

    例如,在F#中,显然使用option<'T>None ,具体而言),而不是像C#中那样使用null 。 为这个场景编写一个包装器生成器应该相当简单:包装器方法将调用原始方法:如果它的返回值是Some x ,则返回x ,否则返回null

    T是一个值类型时,您将需要处理该情况,即不可为空; 你将不得不将wrapper方法的返回值封装到Nullable<T> ,这会让它有点痛苦。

    再次,我相当确定它会在您的方案中写出这样的工具,也许除了定期研究这种库(可以从F#和C#两者无缝使用)之外。 无论如何,我认为这将是一个有趣的实验,我甚至可能会在某个时候进行探索。

    链接地址: http://www.djcxy.com/p/80553.html

    上一篇: Best approach for designing F# libraries for use from both F# and C#

    下一篇: F# vs OCaml: Stack overflow