“AI 技术+人才”如何成为企业增长新引擎?戳此了解>>> 了解详情
写点什么

简化 F#类型提供器的开发

  • 2015-03-26
  • 本文字数:5042 字

    阅读完需:约 17 分钟

类型提供器(Type Provider)是 F# 3.0 版本中最令人感兴趣,也是最强大的特性之一。编写良好的类型提供器能够使在 F#应用程序中进行数据访问的方式更平滑易用,因为这些类型提供器的存在,就无需手动开发及维护任何与底层数据结构相关的类型了。对于数据爆炸型的任务来说,这一点显得尤为重要,因为各种属于互相竞争关系的数据访问技术都需要预先进行大量的配置,才能够发挥作用。

尽管类型提供器存在着这些优势,但它本身的存在却类似于一个黑匣子,只要简单地进行引用,就能够直接发挥作用。我本人并不是那种对魔法咒语般的特性能够感到满意的开发者,因此最近花费了一些时间,对它的功能进行了深入的研究。

我首先创建了一个新的类型提供器,它倒并不像我预计的那样困难。我找到了 FSharp.TypeProviders.StarterPack 这个 NuGet 包,它对 F# 3.0 示例包中所提供的类型进行了封装,以简化类型提供器的创建工作。通过使用这个 NuGet 包,我就能够快速地编写一个类型提供器。

虽然 F# 3.0 中提供的诸多类型确实为我提供了极大的便利性,但令我感到困扰的是,虽然这些类型是作为 F#源代码文件(.fs 文件)编写的,但它们的设计方式却是高度命令式,并且面向对象的,而没有真正地按照 F#的惯例进行设计。这就导致这些代码看上去与 F#原本的代码显得格格不入。让我们看一下以下这个示例,该示例来自于某个 ID3 类型提供器,其中定义了一个新的 ProvidedProperty 实例,首先在其中加入了某些 XML 文档,随后将该属性附加给某个名为 ty 的所提供类型:

复制代码
let prop =
ProvidedProperty(
"AlbumTitle",
typeof<string>,
GetterCode =
fun [tags] ->
<@@ (((%%tags:obj) :?> Dictionary<string, ID3Frame>).["TALB"]).GetContent() |> unbox @@>)
prop.AddXmlDocDelayed (fun () -> "Gets the album title. Corresponds to the TALB tag.")
ty.AddMember prop

在创建提供者方法时,所使用的模式也大致相同,请看以下示例:

复制代码
let method =
ProvidedMethod(
"GetTag",
[ ProvidedParameter("tag", typeof<string>) ],
typeof<ID3Frame option>,
InvokeCode =
(fun [ tags; tag ] -> <@@ let tagDict = ((%%tags:obj) :?> Dictionary<string, ID3Frame>)
if tagDict.ContainsKey(%%tag:string) then Some tagDict.[(%%tag:string)]
else None @@>))
method.AddXmlDocDelayed (fun () -> "Returns an ID3Frame object representing the specific tag")
ty.AddMember method

这两个示例中的语法都很容易理解 —— 尤其是对于具有面向对象开发背景的使用者来说更是如此。但对于 F#编程者而言,这两段代码就显得有些啰嗦了。因为它们使用了中间绑定,这就与管道化或函数组合的方式不太合拍,并且也不符合这门语言本身的精神。如果能够编写一些简单的函数,在其中充分利用 F#的静态解析类型参数的特性,我们就能够极大地改善开发类型提供器的经验。

虽然每个提供器类型类看起来在整个类型提供器设计中都能够占有一席之地,但其中最常用的似乎就是 ProvidedConstructor、ProvidedProperty、ProvidedMethod,和 ProvidedParameter 这么几个类,因此本文余下的部分将特别专注于这几个类型。

我们首先定义一些简单的工厂函数,以封装对应的函数函数,如下所示:(这些函数理应被安置在独立的模块中,出于便利性的考虑,可以将这个模块用 AutoOpen 属性进行装饰。)

复制代码
let inline makeProvidedConstructor parameters invokeCode =
ProvidedConstructor(parameters, InvokeCode = invokeCode)
let inline makeReadOnlyProvidedProperty< ^T> getterCode propName =
ProvidedProperty(propName, typeof< ^T>, GetterCode = getterCode)
let inline makeProvidedMethod< 'T> parameters invokeCode methodName =
ProvidedMethod(methodName, parameters, typeof< 'T>, InvokeCode = invokeCode)
let inline makeProvidedParameter< ^T> paramName =
ProvidedParameter(paramName, typeof< ^T>)

这些函数都比较简单,但还是有一些值得注意的地方。最重要的一点在于:通过将这些函数编写为柯里化(curried)函数,就能够方便地组合某些专门的函数,通过部分应用的方式更好地表达出提供器成员的实际意图。举例来说,在我们假想的 ID3 标签类型提供器示例中,我们可以为每个 ID3 标签暴露出个别的属性。我们无需为每个标签编写重复的代码,其中只有属性和标签的名称不同,而是可以构建一个新的 makeTagProperty 函数,让它将属性类型设为字符串,接受标签和属性的名称,并自动地编译 getter 方法的代码表达式。

其次,每个函数都包括一个内联的修改器。这就给编译器发出了一条指令,在调用端准备调用时,自动插入方法体,以取代函数调用,以此消除函数调用所造成的内存开销。内联修改器通常是与操作符一同使用的,但在这种类型的函数中也能够起到作用。

这些函数中最有趣的一点(除了 makeProvidedConstructor 这个函数),当属它们对于泛型的应用了。在每个函数中都使用了泛型,对提供器成员的返回类型进行了部分抽象。我个人倾向于使用这种方式,因为它提供了与.NET Framework 中的其它部分的一致性,并且将对这些函数的 typeof 的调用独立出来了。除了抽象以外,泛型化所提供的更重要的作用在于:泛型定义中使用了静态解析的类型参数,而不是常规的类型参数。这里的参数是由 ^ 前缀所指定的,而不是常规类型参数中所使用的标准的单引号前缀。

这一区别在此处显得尤为重要,因为它影响着类型提供器的编译行为。在常规的泛型使用中,类型参数是在运行时被解析的,正如 C#和 Visual Basic 中的常见行为一样。而在 F#中使用静态解析的类型参数的情况下,参数的类型则是在编译时进行解析的,通常来说会产生效率更高的代码。此外,静态解析的类型参数也允许使用一些额外的限制类型,这些限制类型通常来说在常规的类型参数中是不允许使用的。这种限制类型的一个例子是成员限制类型,稍后将会对此进行详细说明。

至此,我们从这些帮助函数身上所获得的真正好处只是对某些构造函数的封装。虽然这种方式的确提供了某些额外的便利性,但它们对于帮助我们实现最终的目标:编写更地道的 F#代码来说并没有赶到多少作用。为了达到这一目标,让我们将视线转到原始代码示例中的 AddXmlDocDelayed 方法看看。

如果能够将附加 XML 文档至成员这一步骤实现为管道或组织链中的其中一环,那就再好不过了。虽然在我们至今所讨论过的每个提供器类型中都存在着 AddXmlDocDelayed 方法(除了 ProvidedParameter 方法之外),但这些方法是互相独立的 —— 这意味着并不存在某个单一的、统一的接口可供我们在创建新的函数时进行引用。实际上,每个提供器成员类型只是简单地继承于一个相应的 MemberInfo 实现,并且未引用任何其它的接口。因此我们只剩下这几个选择:

  • 为每个提供器成员类型创建个别的函数,
  • 使用反射,
  • 使用动态的类型 - 测试模式,以获得正确的类型以及相应的方法,或者……
  • 从 F#中的静态解析类型参数中借用某些黑魔法的力量,编写一个函数限制,让它只作用于那些包括了 AddXmlDocDelayed 方法的类型。

前三个选择都没有什么吸引力。为每个支持的提供器类型编写独立的函数很可能会造成将来的各种维护问题。反射是一种可行的方案,但需要编写额外的错误处理逻辑,并且无法提供任何编译期的支持。动态类型 - 测试模式更符合 F#的语法风格,当然也能够提供编译期检查的功能,但我们必须为每个允许的类型提供相应的 match 时的代码。只有通过使用一个成员受限的静态解析类型参数,我们才能够在获得编译期检查功能的同时,不需要显式地指定我们所操作的类型。

以下是该函数的完整形式:

复制代码
let inline addDelayedXmlComment comment providedMember =
(^a : (member AddXmlDocDelayed : (unit -> string) -> unit) providedMember, (fun () -> comment))
providedMember

除去函数签名之外,addDelayedXmlComment 函数只包含了两行代码而已。与之前所示的工厂函数不同,由于从函数的签名中去除了显式的类型参数,而是将这些细节安置在函数体内,因此编译器能够对函数进行更好的推断。

该函数体的第一行的作用,可以比喻为使用反射获得对某个 MethodInfo 实例的引用,该实例代表了该类型的 AddXmlDocDelayed 方法,并调用该实例的 Invoke 方法。区别在于,解析过程是发生在编译期的。在这里,我们所“反射”的类型由 ^a 所表示,这暗示着它是一个静态解析的类型参数。随后,成员限制指明了该 ^a 对象必须具有一个名为 AddXmlDocDelayed 的成员函数,它接受一个函数(unit -> string),并返回 unit 类型。最后,我们以元组形式提供方法的首个形参 providedMember 的实参,就如同我们为 MethodInfo.Invoke 方法所提供的对象参数一样,而第二个实参的内容则是在 AddXmlDocDelayed 方法中用于生成注释的函数。

该函数的第二行只是简单地返回了提供器成员,这就使该提供器成员能够继续在函数调用链中进行传递。

通过这些新的帮助函数,我们就能够改写原始的、命令式的提供器属性代码,如下所示:

复制代码
"AlbumTitle"
|> makeReadOnlyProvidedProperty<string>
(fun [tags] ->
<@@ (((%%tags:obj) :?> Dictionary<string, ID3Frame>).["TALB"]).GetContent() |> unbox @@>))
|> addDelayedXmlComment "Gets the album title. Corresponds to the TALB tag."
|> ty.AddMember

而提供器方法也能够按照如下方式进行重写:

复制代码
"GetTag"
|> makeProvidedMethod<ID3Frame option>
[ makeProvidedParameter<string> "tag" ]
(fun [ tags; tag ] -> <@@ let tagDict = ((%%tags:obj) :?> Dictionary<string, ID3Frame>)
if tagDict.ContainsKey(%%tag:string) then Some tagDict.[(%%tag:string)]
else None @@>)
|> addDelayedXmlComment "Returns an ID3Frame object representing the specific tag" |> ty.AddMember

此外,通过管道,我们能够很清晰地看到:我们定义了一个只读字符串属性或方法,附加了某些 XML 文档,并为提供器类型加入了成员。

由于大多数 MP3 文件都包含多个字符串标签,因此每个标签的提供器属性代码很可能是重复的。我们在此不会重复这些代码,让它们仅仅存在标签名和注释文本的不同。我们将进一步使用的帮助函数中的某些应用,以组成一个特定的工厂函数:

复制代码
let inline makeTagPropertyWithComment tag comment =
let expr =
fun [tags] ->
<@@ (((%%tags:obj) :?> Dictionary<string, ID3Frame>).[tag]).GetContent() |> unbox @@>
(makeReadOnlyProvidedProperty<string> expr) >> (addDelayedXmlComment comment)

makeTagPropertyWithComment 函数中使用了向前组合操作符,以组合出一个新函数,它首先创建了提供器的属性,随后添加了延迟的 XML 注释信息。该函数的返回值正是提供器的属性,正如其签名所显示的一样:

复制代码
string -> string -> (string -> ProvidedProperty)

作为结果,我们能够将提供器属性返回结果随意传递给其它函数。通过使用这一函数,我们的标签属性能够被进一步简化,如下所示:

复制代码
"AlbumTitle"
|> makeTagPropertyWithComment tag "Gets the album title. Corresponds to the TALB tag."
|> ty.AddMember

这一最终版本代码与原始代码之间的区别可谓令人惊叹。通过使用某些简单的内联封装函数,加上静态解析的类型参数,以及成员限制,我们最终成功地将创建一个提供器属性、添加延迟 XML 文档、并将该属性附加至某一类型这一系列任务的代码量减少了约 60%。在此过程中,由于我们消除了中间变量(prop),并将大多数直接方法调用用管道函数进行取代,最终代码变得更为符合 F#的风格。接下来,我们要做的是就是遵循以上所述的相同模式,将这些示例进行扩展,为各种提供器类型提供额外的功能。

如果这些技术能有部分出现在将来的 F#版本中,或是出现在类型提供器初学者包中,那就再好不过了。也许现在正是时候提交一个 pull 请求了!

关于作者

Dave Fancher基于.NET Framework 进行软件开发已经超过十年了。在印第安纳州开发者社区中,他已经是一张熟悉的老面孔了,不仅作为一名演讲者,同时也作为在全美的各种用户组中的积极参与者。在 2013 年七月,Dave 被评选为 Visual F#方向的一名 Microsoft MVP(最有价值专家)。如果他不在编码,或者不在 davefancher.com 上撰写关于代码方面的文章,那么他通常会选择看电影,或是在 Xbox One 上打游戏。

查看英文原文: Simplifying F# Type Provider Development

2015-03-26 11:451431
用户头像

发布了 428 篇内容, 共 171.4 次阅读, 收获喜欢 36 次。

关注

评论

发布
暂无评论
发现更多内容

什么样的家庭条件,现在还招人?

赵新龙

TGO鲲鹏会 招聘

软件测试/测试开发丨Selenium Web自动化测试 高级控件交互方法

测试人

Python 程序员 软件测试 自动化测试 selenium

想学习Python网络爬虫?只需要这一篇文章就够了

这我可不懂

Python 网络爬虫

愿意折腾、相信未来的年青人,都在2050

赵新龙

TGO鲲鹏会 2050

趣步运动挖矿模式dapp系统开发经济模型分析

西安链酷科技

区块链游戏 NFT链游 链游开发

Navicat Premium 16 Mac(多连接数据库管理工具)v16.2.7中文破解版

晴雯哥

墨迹天气黄耀海:深耕气象技术并与服务场景深度融合,AI全面激活气象服务的跃迁

Geek_2d6073

码了!6个提升程序员软技能与效率的必备工具

这我可不懂

工具 前端工具

如何选择可靠的海外住宅代理ip?稳定性测试必不可少

摘星星的猫

汇聚开源力量,赋能软件发展,开放原子开源基金会携开源展区亮相软博会,开源展区现场引关注!

开放原子开源基金会

开源

持币生息理财模式系统开发(源码搭建)

西安链酷科技

DAPP系统开发 积分商城 链上商城

低代码是什么?看这篇就够了

高端章鱼哥

软件开发 低代码

Apache Doris 2.0.1 & 1.2.7 版本正式发布!

SelectDB

数据库 大数据 优化 apache 社区

dapp开发公司价格

西安链酷科技

区块链 DAPP系统开发

西安链游开发 链游开发成本 链游开发周期

西安链酷科技

智能合约 DAPP系统开发 链游开发

打造次世代分析型数据库(一):CDW PG全面升级

腾讯云大数据

数据库

校源行丨2023开放原子校源行开源大使名单

开放原子开源基金会

优雅编码!Java与MongoDB的创新数据库架构

树上有只程序猿

Java mongodb 优雅编码

JetBrains WebStorm v2023.2.1 for Mac(智能javascript语言集成开发环境)

晴雯哥

区块链媒体宣发:区块链项目推广一站式服务商

西安链酷科技

新媒体 媒体服务

充满信心地发布您的代币:我们专业的 IDO 代币启动板开发服务

区块链软件开发推广运营

数字藏品开发 dapp开发 区块链开发 链游开发 NFT开发

Databend 开源周报第 109 期

Databend

龙蜥携手平头哥完成算能 SG2042 平台支持,推动 RISC-V 持续向高性能应用演进

OpenAnolis小助手

芯片 risc-v 龙蜥社区 平头哥 2023 RISC-V 中国峰会

接入 NVIDIA A100、吞吐量提高 10 倍!Milvus GPU 版本使用指南

Zilliz

nvidia 非结构化数据 Milvus Zilliz 向量数据库

喜讯!极限科技再次中标中国移动云 Elasticsearch 自研版技术开发服务项目!

极限实验室

elasticsear 极限科技 中国移动云

查询性能提升10倍!喜马拉雅广告倒排索引设计实践

喜马拉雅技术团队

lucene 性能优化 倒排索引 喜马拉雅

OpenAI公司说它是“实现通用人工智能最有前途方法之一”

博文视点Broadview

Text Workflow for mac(文本工作流程) 1.6.5中文激活版

mac

苹果mac Windows软件 Text Workflow

ipp swap质押挖矿模式系统开发方案

V\TG【ch3nguang】

挖矿矿池系统开发案例

孙文龙理事长参加第二十五届中国国际软件博览会开幕式并发表主题演讲

开放原子开源基金会

达芬奇DaVinci Resolve Studio 18 for Mac(达芬奇调色软件) 18.5.1中文激活版

mac

达芬奇 苹果mac Windows软件 调色软件

简化F#类型提供器的开发_.NET_Dave Fancher_InfoQ精选文章