gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-12-01 20:00:11 24 4
gpt4 key购买 nike

我正在尝试用 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#!现在我必须包装所有标准 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#项目,调用if FooBarCs并编译成FooFar.dll
  • 在源代码级别重用第一个 F# 项目。
  • 创建 .fsi 文件,该文件隐藏该项目中的所有内容。
  • 创建 .fsi 文件,以 C# 方式公开库,使用 C# 习语作为名称、命名空间等。
  • 创建委托(delegate)给核心库的包装器,在必要时进行转换。

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

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

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

    编辑:澄清一下,问题不仅与函数和委托(delegate)有关,而且与使用 F# 库的 C# 用户的整体体验有关。这包括 C# 原生的命名空间、命名约定、习惯用法等。基本上,C# 用户不应该能够检测到该库是用 F# 编写的。反之亦然,F# 用户应该感觉像是在处理 C# 库。

    编辑 2:

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

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

    该文档分为两个主要部分:1) 针对 F# 用户的指南; 2) 指导方针
    面向 C# 用户。它甚至都没有试图假装可以拥有制服
    方法,正好呼应了我的问题:我们可以针对 F#,也可以针对 C#,但是什么是
    针对两者的实用解决方案?

    提醒一下,目标是拥有一个用 F# 编写的库,并且可以从
    F# 和 C# 语言。

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

    现在来看我直接从的例子
    F# Component Design Guidelines .
  • 模块+函数 (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 .
  • 元组的使用
  • F#:在适合返回值时使用元组。
  • C#:避免在 vanilla .NET API 中使用元组作为返回值。
  • 异步
  • F#:务必在 F# API 边界使用 Async 进行异步编程。
  • C#:使用 .NET 异步编程模型公开异步操作
    (BeginFoo, EndFoo),或作为返回 .NET 任务 (Task) 的方法,而不是作为 F# Async
    对象。
  • 使用 Option
  • F#:考虑对返回类型使用选项值而不是引发异常(对于面向 F# 的代码)。
  • 考虑使用 TryGetValue 模式,而不是在 vanilla 中返回 F# 选项值(选项)
    .NET API,并且更喜欢方法重载而不是将 F# 选项值作为参数。
  • 受歧视的工会
  • F#:使用可区分联合作为类层次结构的替代方法来创建树结构数据
  • C#:没有具体的指导方针,但歧视联合的概念对 C# 来说是陌生的
  • curry 函数
  • F#:柯里化函数是 F# 的惯用语
  • C#:不要在 vanilla .NET API 中使用柯里化参数。
  • 检查空值
  • F#:这不是 F# 的惯用语
  • C#:考虑在 vanilla .NET API 边界上检查空值。
  • F# 类型的使用 list , map , set
  • F#:在 F# 中使用这些是惯用的
  • C#:考虑使用 .NET 集合接口(interface)类型 IEnumerable 和 IDictionary
    用于 vanilla .NET API 中的参数和返回值。 (即不要使用 F# listmapset )
  • 函数类型(最明显的一种)
  • F#:使用 F# 函数作为值是 F# 的习惯用法,显然
  • C#:在 vanilla .NET API 中优先使用 .NET 委托(delegate)类型而不是 F# 函数类型。

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

    顺便说一句,指南也有部分答案:

    ... a common implementation strategy when developing higher-order methods for vanilla .NET libraries is to author all the implementation using F# function types, and then create the public API using delegates as a thin façade atop the actual F# implementation.



    总结一下。

    有一个明确的答案:我没有遗漏任何编译器技巧。

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

    那么问题仍然是关于这个的实际实现:
  • 单独的组件?或
  • 不同的命名空间?

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

    我想我会同意这一点,因为命名空间的选择是这样的
    不会让 .NET/C# 用户感到惊讶或混淆,这意味着命名空间
    对于他们来说,它应该看起来像是他们的主要命名空间。该
    F# 用户将不得不承担选择特定于 F# 的命名空间的负担。
    例如:
  • FSharp.Foo.Bar -> 面向库的 F# 命名空间
  • Foo.Bar -> .NET 包装器的命名空间,C# 惯用语
  • 最佳答案

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

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

  • F# 库 设计为仅在 F# 中使用,因此它的公共(public)接口(interface)以函数式风格编写(使用 F# 函数类型、元组、可区分联合等)
  • .NET 库 旨在用于任何 .NET 语言(包括 C# 和 F#),并且它通常遵循 .NET 面向对象的风格。这意味着您将大部分功能公开为带有方法的类(有时是扩展方法或静态方法,但大多数代码应该在 OO 设计中编写)。

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

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

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

    关于c# - 设计用于 F# 和 C# 的 F# 库的最佳方法,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/10110174/

    24 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com