我正在尝试设计一个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
,我想要的是Func
和Action
,像这样:
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
包装成 Func
和 Action
,就像这样:
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#用户,另一个面向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#组件设计指南文档(非常感谢@gradbot!)。如果使用该文档中的指南,则可以解决其中的某些问题,但并非全部。
该文档分为两个主要部分:1)针对F#用户的指南;2)针对C#用户的指南。它甚至没有试图假装有可能拥有统一的方法,这正好回应了我的问题:我们可以针对F#,我们可以针对C#,但针对两者的实际解决方案是什么?
提醒一下,目标是编写一个F#库,可以从F#和C#语言中“惯用地”使用。关键词在于“惯用地”。问题不在于通用的互操作性,而是在于可以在不同的语言中使用库的情况。
现在来看一些例子,这些例子直接来自F#组件设计指南。
Modules+functions (F#) vs Namespaces+Types+functions
F#: Do use namespaces or modules to contain your types and modules. The idiomatic use is to place functions in modules, e.g.:
// 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.
This is incompatible with the F# guideline, and the example would need to be re-written to fit the C# users:
[<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
andzoo
without prefixing it withFoo
.
Use of tuples
F#: Do use tuples when appropriate for return values.
C#: Avoid using tuples as return values in vanilla .NET APIs.
Async
F#: Do use Async for async programming at F# API boundaries.
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.
Use of
Option
F#: Consider using option values for return types instead of raising exceptions (for F#-facing code).
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.
Discriminated unions
F#: Do use discriminated unions as an alternative to class hierarchies for creating tree-structured data
C#: no specific guidelines for this, but the concept of discriminated unions is foreign to C#
Curried functions
F#: curried functions are idiomatic for F#
C#: Do not use currying of parameters in vanilla .NET APIs.
Checking for null values
F#: this is not idiomatic for F#
C#: Consider checking for null values on vanilla .NET API boundaries.
Use of F# types
list
,map
,set
, etcF#: it is idiomatic to use these in F#
C#: Consider using the .NET collection interface types IEnumerable and IDictionary for parameters and return values in vanilla .NET APIs. (i.e. do not use F#
list
,map
,set
)
Function types (the obvious one)
F#: use of F# functions as values is idiomatic for F#, obviously
C#: Do use .NET delegate types in preference to F# function types in vanilla .NET APIs.
我认为这些足以展示我的问题的性质。
顺便说一下,指南中也有部分答案:
......在为普通.NET库开发高阶方法时,常见的实现策略是使用F#函数类型编写所有实现, 然后使用委托创建公共API作为实际F#实现的薄外观。
总结。
有一个明确的答案:我没有遗漏任何编译器技巧。
根据指南文档,似乎首先为F#进行编写,然后创建.NET包装器外壳是一种合理的策略。
然后问题仍然关于这个的实际实现:
分离的程序集? 或者
不同的命名空间?
如果我的解释正确,Tomas建议使用单独的命名空间应该足够,并且应该是可接受的解决方案。
考虑到命名空间的选择不会让.NET/C#用户感到惊讶或混淆,我认为我会同意这一点,这意味着对于他们来说,命名空间可能需要看起来像是他们的主要命名空间。F#用户将需要承担选择特定于F#的命名空间的负担。例如:
FSharp.Foo.Bar -> F#面向库的命名空间
Foo.Bar -> .NET包装器的命名空间,符合C#惯用法