我正在尝试在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#编写的库的一流体验?
到目前为止,我没有比这两种方法更好的方法了:
两个单独的程序集:一个针对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#的惯用用法,因为
如果没有bar
和zoo
前缀,我们将无法再使用。
元组的使用
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#类型Option
,list
,map
等
F#:在F#中使用它们是惯用的
C#:考虑使用.NET集合接口类型IEnumerable和IDictionary
原始.NET API中的参数和返回值。 (即,不要使用F#set
,list
,map
)
函数类型(显而易见的一种)
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
这样的函数是太低级的概念。您可以将它们公开为使用Func
和Action
的方法,但这可能不是您一开始就设计普通的.NET库的方式(也许您会使用Builder模式或类似的方式) 。
在某些情况下(例如,当设计的数字库与.NET库样式不太匹配时),设计将F#和.NET样式混合在一个库中的库是很有意义的。最好的方法是拥有普通的F#(或普通的.NET)API,然后以其他样式自然使用包装器。包装器可以位于单独的命名空间中(例如MyLibrary.FSharp
和MyLibrary
)。
在您的示例中,您可以将F#实现保留在MyLibrary.FSharp
中,然后在MyLibrary
名称空间中添加.NET(C#友好)包装器(类似于Daniel发布的代码)作为某些类的静态方法。但同样,.NET库可能具有比函数组成更特定的API。
我有一个包含成员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/…