简体   繁体   English

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

[英]Best approach for designing F# libraries for use from both F# and C#

I am trying to design a library in F#.我正在尝试用 F# 设计一个库。 The library should be friendly for use from both F# and C# .该库应该适合从F# 和 C# 使用

And this is where I'm stuck a little bit.这就是我被卡住的地方。 I can make it F# friendly, or I can make it C# friendly, but the problem is how to make it friendly for both.我可以使它对 F# 友好,也可以使它对 C# 友好,但问题是如何使它对两者都友好。

Here is an example.这是一个例子。 Imagine I have the following function in F#:想象一下,我在 F# 中有以下函数:

let compose (f: 'T -> 'TResult) (a : 'TResult -> unit) = f >> a

This is perfectly usable from F#:这在 F# 中完全可用:

let useComposeInFsharp() =
    let composite = compose (fun item -> item.ToString) (fun item -> printfn "%A" item)
    composite "foo"
    composite "bar"

In C#, the compose function has the following signature:在 C# 中, compose函数具有以下签名:

FSharpFunc<T, Unit> compose<T, TResult>(FSharpFunc<T, TResult> f, FSharpFunc<TResult, Unit> a);

But of course, I don't want FSharpFunc in the signature, what I want is Func and Action instead, like this:但是当然,我不想在签名中使用FSharpFunc ,我想要的是FuncAction ,就像这样:

Action<T> compose2<T, TResult>(Func<T, TResult> f, Action<TResult> a);

To achieve this, I can create compose2 function like this:为了实现这一点,我可以像这样创建compose2函数:

let compose2 (f: Func<'T, 'TResult>) (a : Action<'TResult> ) = 
    new Action<'T>(f.Invoke >> a.Invoke)

Now, this is perfectly usable in C#:现在,这在 C# 中完全可用:

void UseCompose2FromCs()
{
    compose2((string s) => s.ToUpper(), Console.WriteLine);
}

But now we have a problem using compose2 from F#!但是现在我们在使用 F# 中的compose2了问题! Now I have to wrap all standard F# funs into Func and Action , like this:现在我必须将所有标准 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"

The question: How can we achieve first-class experience for the library written in F# for both F# and C# users?问题:我们如何才能为 F# 和 C# 用户实现用 F# 编写的库的一流体验?

So far, I couldn't come up with anything better than these two approaches:到目前为止,我想不出比这两种方法更好的方法:

  1. Two separate assemblies: one targeted to F# users, and the second to C# users.两个独立的程序集:一个面向 F# 用户,另一个面向 C# 用户。
  2. One assembly but different namespaces: one for F# users, and the second for C# users.一个程序集但不同的命名空间:一个用于 F# 用户,第二个用于 C# 用户。

For the first approach, I would do something like this:对于第一种方法,我会做这样的事情:

  1. Create a F# project, call it FooBarFs and compile it into FooBarFs.dll.创建一个 F# 项目,将其命名为 FooBarFs 并将其编译为 FooBarFs.dll。

    • Target the library purely to F# users.该库仅面向 F# 用户。
    • Hide everything unnecessary from the .fsi files.隐藏 .fsi 文件中不需要的所有内容。
  2. Create another F# project, call if FooBarCs and compile it into FooFar.dll创建另一个F#项目,调用if FooBarCs并编译成FooFar.dll

    • Reuse the first F# project at the source level.在源代码级别重用第一个 F# 项目。
    • Create .fsi file which hides everything from that project.创建 .fsi 文件,该文件隐藏该项目中的所有内容。
    • Create .fsi file which exposes the library in C# way, using C# idioms for name, namespaces, etc.创建 .fsi 文件,该文件以 C# 方式公开库,使用 C# 习语作为名称、命名空间等。
    • Create wrappers that delegate to the core library, doing the conversion where necessary.创建委托给核心库的包装器,在必要时进行转换。

I think the second approach with the namespaces can be confusing to the users, but then you have one assembly.我认为使用命名空间的第二种方法可能会让用户感到困惑,但是你有一个程序集。

The question: None of these are ideal, perhaps I am missing some kind of compiler flag/switch/attribute or some kind of trick and there is a better way of doing this?问题:这些都不理想,也许我缺少某种编译器标志/开关/属性或某种技巧,并且有更好的方法来做到这一点?

The question: has anyone else tried to achieve something similar and if so how did you do it?问题:有没有其他人试图实现类似的目标,如果是这样,你是如何做到的?

EDIT: to clarify, the question is not only about functions and delegates but the overall experience of a C# user with an F# library.编辑:澄清一下,问题不仅与函数和委托有关,而且与使用 F# 库的 C# 用户的整体体验有关。 This includes namespaces, naming conventions, idioms and suchlike that are native to C#.这包括 C# 原生的命名空间、命名约定、习惯用法等。 Basically, a C# user shouldn't be able to detect that the library was authored in F#.基本上,C# 用户不应该能够检测到该库是用 F# 编写的。 And vice versa, an F# user should feel like dealing with a C# library.反之亦然,F# 用户应该感觉像是在处理 C# 库。


EDIT 2:编辑2:

I can see from the answers and comments so far that my question lacks the necessary depth, perhaps mostly due to use of only one example where interoperability issues between F# and C# arise, the issue of function values.从目前的答案和评论中可以看出,我的问题缺乏必要的深度,这可能主要是由于仅使用了一个示例,其中出现了 F# 和 C# 之间的互操作性问题,即函数值问题。 I think this is the most obvious example and so this led me to use it to ask the question, but by the same token gave the impression that this is the only issue I am concerned with.我认为这是最明显的例子,所以这让我用它来提出问题,但同样的原因给人的印象是这是我唯一关心的问题。

Let me provide more concrete examples.让我提供更具体的例子。 I have read through the most excellent F# Component Design Guidelines document (many thanks @gradbot for this!).我已经阅读了最优秀的F# 组件设计指南文档(非常感谢@gradbot!)。 The guidelines in the document, if used, do address some of the issues but not all.文档中的指南(如果使用)确实解决了一些问题,但不是全部。

The document is split into two main parts: 1) guidelines for targeting F# users;该文档分为两个主要部分:1) 针对 F# 用户的指南; and 2) guidelines for targeting C# users.和 2) 针对 C# 用户的指南。 Nowhere does it even attempt to pretend that it is possible to have a uniform approach, which exactly echoes my question: we can target F#, we can target C#, but what is the practical solution for targeting both?它甚至没有试图假装可能有一个统一的方法,这恰好回应了我的问题:我们可以针对 F#,也可以针对 C#,但是针对两者的实际解决方案是什么?

To remind, the goal is to have a library authored in F#, and which can be used idiomatically from both F# and C# languages.提醒一下,我们的目标是拥有一个用 F# 编写的库,并且可以在 F# 和 C# 语言中惯用地使用它。

The keyword here is idiomatic .这里的关键字是惯用的 The issue is not the general interoperability where it is just possible to use libraries in different languages.问题不在于可以使用不同语言的库的一般互操作性。

Now to the examples, which I take straight from F# Component Design Guidelines .现在来看我直接从F# 组件设计指南中获取的示例。

  1. Modules+functions (F#) vs Namespaces+Types+functions模块+函数 (F#) 与命名空间+类型+函数

    • F#: Do use namespaces or modules to contain your types and modules. F#:请使用命名空间或模块来包含您的类型和模块。 The idiomatic use is to place functions in modules, eg:惯用的用法是在模块中放置函数,例如:

       // library module Foo let bar() = ... let zoo() = ... // Use from F# open Foo bar() zoo()
    • C#: Do use namespaces, types and members as the primary organizational structure for your components (as opposed to modules), for vanilla .NET APIs. C#:对于 vanilla .NET API,务必使用命名空间、类型和成员作为组件(而不是模块)的主要组织结构。

      This is incompatible with the F# guideline, and the example would need to be re-written to fit the C# users:这与 F# 指南不兼容,并且需要重新编写示例以适合 C# 用户:

       [<AbstractClass; Sealed>] type Foo = static member bar() = ... static member zoo() = ...

      By doing so though, we break the idiomatic use from F# because we can no longer use bar and zoo without prefixing it with Foo .但是,通过这样做,我们打破了 F# 的惯用用法,因为我们不能再使用barzoo而不用Foo前缀。

  2. Use of tuples元组的使用

    • F#: Do use tuples when appropriate for return values. F#:在适合返回值时使用元组。

    • C#: Avoid using tuples as return values in vanilla .NET APIs. C#:避免在 vanilla .NET API 中使用元组作为返回值。

  3. Async异步

    • F#: Do use Async for async programming at F# API boundaries. F#:务必在 F# API 边界使用 Async 进行异步编程。

    • C#: Do expose asynchronous operations using either the .NET asynchronous programming model (BeginFoo, EndFoo), or as methods returning .NET tasks (Task), rather than as F# Async objects. C#:使用 .NET 异步编程模型(BeginFoo、EndFoo)或作为返回 .NET 任务(Task)的方法,而不是作为 F# 异步对象公开异步操作。

  4. Use of Option Option使用

    • F#: Consider using option values for return types instead of raising exceptions (for F#-facing code). F#:考虑对返回类型使用选项值而不是引发异常(对于面向 F# 的代码)。

    • Consider using the TryGetValue pattern instead of returning F# option values (option) in vanilla .NET APIs, and prefer method overloading over taking F# option values as arguments.考虑使用 TryGetValue 模式而不是在 vanilla .NET API 中返回 F# 选项值(选项),并且更喜欢方法重载而不是将 F# 选项值作为参数。

  5. Discriminated unions受歧视的工会

    • F#: Do use discriminated unions as an alternative to class hierarchies for creating tree-structured data F#:使用可区分联合作为类层次结构的替代方法来创建树结构数据

    • C#: no specific guidelines for this, but the concept of discriminated unions is foreign to C# C#:没有具体的指导方针,但歧视联合的概念对 C# 来说是陌生的

  6. Curried functions咖喱函数

    • F#: curried functions are idiomatic for F# F#:柯里化函数是 F# 惯用的

    • C#: Do not use currying of parameters in vanilla .NET APIs. C#:不要在 vanilla .NET API 中使用柯里化参数。

  7. Checking for null values检查空值

    • F#: this is not idiomatic for F# F#:这不是 F# 的惯用语

    • C#: Consider checking for null values on vanilla .NET API boundaries. C#:考虑在 vanilla .NET API 边界上检查空值。

  8. Use of F# types list , map , set , etc使用 F# 类型listmapset

    • F#: it is idiomatic to use these in F# F#:在 F# 中使用这些是惯用的

    • C#: Consider using the .NET collection interface types IEnumerable and IDictionary for parameters and return values in vanilla .NET APIs. C#:考虑将 .NET 集合接口类型 IEnumerable 和 IDictionary 用于 vanilla .NET API 中的参数和返回值。 ( ie do not use F# list , map , set ) 即不使用 F# list , map , set

  9. Function types (the obvious one)函数类型(最明显的一种)

    • F#: use of F# functions as values is idiomatic for F#, obviously F#:显然,使用 F# 函数作为值是 F# 的惯用语

    • C#: Do use .NET delegate types in preference to F# function types in vanilla .NET APIs. C#:在 vanilla .NET API 中优先使用 .NET 委托类型而不是 F# 函数类型。

I think these should be sufficient to demonstrate the nature of my question.我认为这些应该足以证明我的问题的性质。

Incidentally, the guidelines also have a partial answer:顺便说一句,指南也有部分答案:

... 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. ... 为 vanilla .NET 库开发高阶方法时的一个常见实现策略是使用 F# 函数类型编写所有实现,然后使用委托创建公共 API 作为实际 F# 实现之上的薄外观。

To summarise.总结一下。

There is one definite answer: there are no compiler tricks that I missed .有一个明确的答案:我没有遗漏任何编译器技巧

As per the guidelines doc, it seems that authoring for F# first and then creating a facade wrapper for .NET is a reasonable strategy.根据指南文档,似乎首先为 F# 创作然后为 .NET 创建外观包装器是一个合理的策略。

The question then remains regarding the practical implementation of this:那么问题仍然是关于这个的实际实施:

  • Separate assemblies?单独的组件? or要么

  • Different namespaces?不同的命名空间?

If my interpretation is correct, Tomas suggests that using separate namespaces should be sufficient, and should be an acceptable solution.如果我的解释是正确的,Tomas 建议使用单独的命名空间就足够了,并且应该是一个可以接受的解决方案。

I think I will agree with that given that the choice of namespaces is such that it does not surprise or confuse the .NET/C# users, which means that the namespace for them should probably look like it is the primary namespace for them.我想我会同意这一点,因为命名空间的选择不会让 .NET/C# 用户感到惊讶或混淆,这意味着他们的命名空间应该看起来像是他们的主要命名空间。 The F# users will have to take the burden of choosing F#-specific namespace. F# 用户将不得不承担选择特定于 F# 的命名空间的负担。 For example:例如:

  • FSharp.Foo.Bar -> namespace for F# facing the library FSharp.Foo.Bar -> 面向库的 F# 命名空间

  • Foo.Bar -> namespace for .NET wrapper, idiomatic for C# Foo.Bar -> .NET 包装器的命名空间,C# 惯用的命名空间

Daniel already explained how to define a C#-friendly version of the F# function that you wrote, so I'll add some higher-level comments. Daniel 已经解释了如何定义您编写的 F# 函数的 C# 友好版本,因此我将添加一些更高级别的注释。 First of all, you should read the F# Component Design Guidelines (referenced already by gradbot).首先,您应该阅读F# 组件设计指南(已被 gradbot 引用)。 This is a document that explains how to design F# and .NET libraries using F# and it should answer many of your questions.这是一个解释如何使用 F# 设计 F# 和 .NET 库的文档,它应该可以回答您的许多问题。

When using F#, there are basically two kinds of libraries you can write:使用 F# 时,基本上可以编写两种库:

  • F# library is designed to be used only from F#, so it's public interface is written in a functional style (using F# function types, tuples, discriminated unions etc.) F# 库被设计为只能在 F# 中使用,因此它的公共接口是以函数式风格编写的(使用 F# 函数类型、元组、可区分联合等)

  • .NET library is designed to be used from any .NET language (including C# and F#) and it typically follows .NET object-oriented style. .NET 库旨在用于任何.NET 语言(包括 C# 和 F#),它通常遵循 .NET 面向对象的风格。 This means that you'll expose most of the functionality as classes with method (and sometimes extension methods or static methods, but mostly the code should be written in the OO design).这意味着您将大部分功能公开为带有方法的类(有时是扩展方法或静态方法,但大多数代码应该在 OO 设计中编写)。

In your question, you're asking how to expose function composition as a .NET library, but I think that functions like your compose are too low level concepts from the .NET library point of view.在您的问题中,您询问如何将函数组合公开为 .NET 库,但我认为从 .NET 库的角度来看,像您的compose这样的函数是太低级的概念。 You can expose them as methods working with Func and Action , but that probably isn't how you would design a normal .NET library in the first place (perhaps you'd use the Builder pattern instead or something like that).您可以将它们公开为使用FuncAction ,但这可能不是您首先设计普通 .NET 库的方式(也许您会改用 Builder 模式或类似的东西)。

In some cases (ie when designing numerical libraries that do not really fit well with the .NET library style), it makes a good sense to design a library that mixes both F# and .NET styles in a single library.在某些情况下(即在设计与 .NET 库风格不太匹配的数字库时),设计一个在单个库中混合F#.NET风格的库是很有意义的。 The best way to do this is to have normal F# (or normal .NET) API and then provide wrappers for natural use in the other style.最好的方法是使用普通的 F#(或普通的 .NET)API,然后提供包装器以在其他风格中自然使用。 The wrappers can be in a separate namespace (like MyLibrary.FSharp and MyLibrary ).包装器可以位于单独的命名空间中(如MyLibrary.FSharpMyLibrary )。

In your example, you could leave the F# implementation in MyLibrary.FSharp and then add .NET (C#-friendly) wrappers (similar to code that Daniel posted) in the MyLibrary namespace as static method of some class.在您的示例中,您可以将 F# 实现保留在MyLibrary.FSharp ,然后在MyLibrary命名空间中添加 .NET(C# 友好)包装器(类似于 Daniel 发布的代码)作为某个类的静态方法。 But again, .NET library would probably have more specific API than function composition.但同样,.NET 库可能比函数组合具有更具体的 API。

You only have to wrap function values (partially-applied functions, etc) with Func or Action , the rest are converted automatically.你只需要用FuncAction包装函数(部分应用的函数等),其余的会自动转换。 For example:例如:

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

let a = A(1)
a.Invoke(fun i -> i + 1)

So it makes sense to use Func / Action where applicable.因此,在适用的情况下使用Func / Action是有意义的。 Does this eliminate your concerns?这是否消除了您的担忧? I think your proposed solutions are overly-complicated.我认为您提出的解决方案过于复杂。 You can write your entire library in F# and use it pain-free from F# and C# (I do it all the time).您可以用 F# 编写整个库,并在 F#C# 中轻松使用它(我一直都这样做)。

Also, F# is more flexible than C# in terms of interoperability so it's generally best to follow traditional .NET style when this is a concern.此外,在互操作性方面,F# 比 C# 更灵活,因此通常最好遵循传统的 .NET 风格。

EDIT编辑

The work required to make two public interfaces in separate namespaces, I think, is only warranted when they are complementary or the F# functionality is not usable from C# (such as inlined functions, which depend on F#-specific metadata).在不同的命名空间中创建两个公共接口所需的工作,我认为,只有当它们是互补的或者 F# 功能不能从 C# 使用(例如内联函数,它依赖于 F# 特定的元数据)时才需要。

Taking your points in turn:依次拿你的分数:

  1. Module + let bindings and constructor-less type + static members appear exactly the same in C#, so go with modules if you can.模块 + let绑定和无构造函数类型 + 静态成员在 C# 中看起来完全相同,所以如果可以,请使用模块。 You can use CompiledNameAttribute to give members C#-friendly names.您可以使用CompiledNameAttribute为成员提供 C# 友好的名称。

  2. I may be wrong, but my guess is that the Component Guidelines were written prior to System.Tuple being added to the framework.我可能错了,但我的猜测是组件指南是在System.Tuple添加到框架之前编写的。 (In earlier versions F# defined it's own tuple type.) It's since become more acceptable to use Tuple in a public interface for trivial types. (在早期版本中,F# 定义了它自己的元组类型。)从那时起,在普通类型的公共接口中使用Tuple变得更容易接受。

  3. This is where I think you have do things the C# way because F# plays well with Task but C# doesn't play well with Async .这就是我认为你可以用 C# 方式做事的地方,因为 F# 与Task配合得很好,但 C# 与Async配合得不好。 You can use async internally then call Async.StartAsTask before returning from a public method.您可以在内部使用异步,然后在从公共方法返回之前调用Async.StartAsTask

  4. Embrace of null may be the single biggest drawback when developing an API for use from C#.在开发用于 C# 的 API 时,拥抱null可能是一个最大的缺点。 In the past, I tried all kinds of tricks to avoid considering null in internal F# code but, in the end, it was best to mark types with public constructors with [<AllowNullLiteral>] and check args for null.过去,我尝试了各种技巧来避免在内部 F# 代码中考虑 null,但最终,最好使用[<AllowNullLiteral>]标记具有公共构造函数的类型,并检查 args 是否为 null。 It's no worse than C# in this respect.在这方面它并不比 C# 差。

  5. Discriminated unions are generally compiled to class hierarchies but always have a relatively friendly representation in C#.可区分联合通常编译为类层次结构,但在 C# 中始终具有相对友好的表示形式。 I would say, mark them with [<AllowNullLiteral>] and use them.我会说,用[<AllowNullLiteral>]标记它们并使用它们。

  6. Curried functions produce function values , which shouldn't be used.柯里化函数产生函数,不应使用。

  7. I found it was better to embrace null than to depend on it being caught at the public interface and ignore it internally.我发现拥抱 null 比依赖它在公共接口上被捕获并在内部忽略它更好。 YMMV.天啊。

  8. It makes a lot of sense to use list / map / set internally.在内部使用list / map / set很有意义。 They can all be exposed through the public interface as IEnumerable<_> .它们都可以通过公共接口公开为IEnumerable<_> Also, seq , dict , and Seq.readonly are frequently useful.此外, seqdictSeq.readonly经常有用。

  9. See #6.见#6。

Which strategy you take depends on the type and size of your library but, in my experience, finding the sweet spot between F# and C# required less work—in the long run—than creating separate APIs.您采用哪种策略取决于您的库的类型和大小,但根据我的经验,从长远来看,找到 F# 和 C# 之间的最佳点比创建单独的 API 需要更少的工作。

Draft F# Component Design Guidelines (August 2010) F# 组件设计指南草案(2010 年 8 月)

Overview This document looks at some of the issues related to F# component design and coding.概述本文档着眼于与 F# 组件设计和编码相关的一些问题。 In particular, it covers:特别是,它涵盖:

  • Guidelines for designing “vanilla” .NET libraries for use from any .NET language.设计用于任何 .NET 语言的“vanilla”.NET 库的指南。
  • Guidelines for F#-to-F# libraries and F# implementation code. F# 到 F# 库和 F# 实现代码的准则。
  • Suggestions on coding conventions for F# implementation code关于 F# 实现代码的编码约定的建议

Although it probably would be an overkill, you could consider writing an application using Mono.Cecil (it has awesome support on the mailing list) that would automate the conversion on the IL level.尽管这可能有点矫枉过正,但您可以考虑使用Mono.Cecil编写一个应用程序(它在邮件列表上有很棒的支持),可以在 IL 级别自动转换。 For example, you implement your assembly in F#, using the F#-style public API, then the tool would generate a C#-friendly wrapper over it.例如,您在 F# 中使用 F# 样式的公共 API 实现程序集,然后该工具将在其上生成 C# 友好的包装器。

For instance, in F# you would obviously use option<'T> ( None , specifically) instead of using null like in C#.例如,在 F# 中,您显然会使用option<'T> (特别是None )而不是像在 C# 中那样使用null Writing a wrapper generator for this scenario should be fairly easy: the wrapper method would invoke the original method: if it's return value was Some x , then return x , otherwise return null .为这种情况编写包装器生成器应该相当容易:包装器方法将调用原始方法:如果它的返回值为Some x ,则返回x ,否则返回null

You would need to handle the case when T is a value type, ie non-nullable;您需要处理T是值类型(即不可为空)的情况; you would have to wrap the return value of the wrapper method into Nullable<T> , which makes it a bit painful.您必须将包装器方法的返回值包装到Nullable<T> ,这让它有点痛苦。

Again, I'm quite certain that it would pay off to write such a tool in your scenario, maybe except if you'll be working on this such library (usable seamlessly from F# and C# both) regularly.同样,我很确定在您的场景中编写这样一个工具是值得的,也许除非您定期处理这个这样的库(可以从 F# 和 C# 无缝使用)。 In any case, I think it would be an interesting experiment, one that I might even explore sometime.无论如何,我认为这将是一个有趣的实验,我什至可能会在某个时候进行探索。

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

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