设计用于F#和C#的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);
}

但是现在我们在使用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#惯用语来表示名称,名称空间等。
创建委派给核心库的包装器,并在必要时进行转换。

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

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

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

编辑:澄清一下,这个问题不仅涉及函数和委托,而且还涉及具有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#:请使用名称空间,类型和成员作为您的主要组织结构
组件(与模块相对),适用于原始.NET API。

这与F#准则不兼容,该示例将需要
重新编写以适合C#用户:

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

通过这样做,我们打破了F#的惯用用法,因为
如果没有barzoo前缀,我们将无法再使用。

元组的使用

F#:在适合返回值时,请使用元组。
C#:避免将元组用作原始.NET API中的返回值。

异步

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

Foo的使用

F#:考虑将选项值用于返回类型,而不是引发异常(对于面向F#的代码)。
考虑使用TryGetValue模式,而不是在香草中返回F#选项值(选项)
.NET API,并且更喜欢方法重载,而不是将F#选项值作为参数。

歧视工会

F#:请使用区分联合作为类层次结构的替代方法,以创建树结构数据
C#:对此没有具体指导,但歧视性工会的概念对C#来说是陌生的

咖喱函数

F#:咖喱函数对于F#是惯用的
C#:请勿在普通的.NET API中使用临时参数。

检查空值

F#:这不是F#的惯用语
C#:考虑检查香草.NET API边界上的空值。

使用F#类型Optionlistmap

F#:在F#中使用它们是惯用的
C#:考虑使用.NET集合接口类型IEnumerable和IDictionary
原始.NET API中的参数和返回值。 (即,不要使用F#setlistmap

函数类型(显而易见的一种)

F#:使用F#函数作为值对于F#来说是惯用的
C#:在.NET API中,请优先使用.NET委托类型,而不要使用F#函数类型。

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

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

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

总结一下。

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

根据指南文档,似乎先编写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#,因此它的公共接口是以功能样式编写的(使用F#函数类型,元组,区分的并集等)。
.NET库设计为可用于任何.NET语言(包括C#和F#),并且通常遵循.NET面向对象的样式。这意味着您将把大多数功能作为带有方法的类公开(有时是扩展方法或静态方法,但大多数代码应在OO设计中编写)。

在您的问题中,您正在询问如何将函数组成作为.NET库公开,但是从.NET库的角度来看,我认为像compose这样的函数是太低级的概念。您可以将它们公开为使用FuncAction的方法,但这可能不是您一开始就设计普通的.NET库的方式(也许您会使用Builder模式或类似的方式) 。

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

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

将谓词<T>转换为Func <T,bool> - c#

我有一个包含成员Predicate的类,希望在Linq表达式中使用该类:using System.Linq; class MyClass { public bool DoAllHaveSomeProperty() { return m_instrumentList.All(m_filterExpression); } private IEnumerable&…

Div单击与单选按钮相同吗? - php

有没有一种方法可以使div上的click事件与表单环境中的单选按钮相同?我只希望下面的div提交值,单选按钮很丑代码输出如下:<input id="radio-2011-06-08" value="2011-06-08" type="radio" name="radio_date&#…

故障排除“警告:session_start():无法发送会话高速缓存限制器-标头已发送” - php

我收到警告:session_start()[function.session-start]:无法发送会话缓存限制器-标头已发送(错误输出开始如果我将表单数据提交到其他文件进行处理,则可以正常工作。但是,如果我将表单数据提交到同一页面,则会出现此错误。请建议<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0…

Asp.net发送信息表单到页面 - c#

我正在尝试使用弹出窗口中的新信息更新旧页面。到目前为止,我尝试过将结果保存在会话中Session["Data"] = DLvrijecampingplaatsen.SelectedItem; 然后当它达到Page_Load时,将其重新加载回旧页面if (Session["Data"] != null) { LBkies…

调整窗口大小时如何调整YouTube播放器的大小 - php

我想显示包含YouTube视频的弹出窗口。我的问题是当用户调整弹出窗口的大小时如何调整YouTube播放器的大小?弹出窗口的头部分PHP / HTML代码<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/…