写点什么

简化 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:451585
用户头像

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

关注

评论

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

2023年Java学习步骤及路线(超详细)

Java你猿哥

Java 微服务 Spring Boot JVM java面试

人工智能将如何改变敏捷项目管理?

敏捷开发

人工智能 项目管理 AI 敏捷开发 Scrum Master

ChatGPT的原理与前端领域实践 | 京东云技术团队

京东科技开发者

人工智能 前端 ChatGPT 企业号 5 月 PK 榜

Github星标百万!终于有人将Spring技术精髓收录成册

做梦都在改BUG

Java spring 框架

真香!阿里最新产出分布式进阶实战手册,涵盖分布式架构所有操作

Java你猿哥

架构 分布式 微服务 Spring Cloud Spring Boot

救命稻草!阿里P8耗时5月打造的架构师速成手册,千金难求

Java你猿哥

程序员 ssm 软件架构 架构设计 架构师

逆流而上!整合阿里高频考点2023Java岗面试突击指南手册首次亮相

Java你猿哥

Java MySQL redis ssm java面试

使用 PAI-Blade 优化 Stable Diffusion 推理流程

阿里云大数据AI技术

人工智能 推理 Stable Diffusion 企业号 5 月 PK 榜

单机 T 级流量转发吞吐提升 5 倍,可编程负载均衡网关 1.0 上线

百度Geek说

云计算 负载均衡 企业号 5 月 PK 榜

百万级数据导出优化方案

做梦都在改BUG

京东购物车如何提升30%性能 | 京东云技术团队

京东科技开发者

性能优化 RPC 企业号 5 月 PK 榜 京东购物车 异步改造

绕不过的并发编程——synchronized原理

Java你猿哥

Java 并发编程 ssm synchronized

记一次redis主从切换导致的数据丢失与陷入只读状态故障

Java你猿哥

redis sentinel ssm 高可用架构

软件测试 | FTP性能测试脚本开发(2)

测吧(北京)科技有限公司

测试

Kafka生产者你不得不知的那些事儿

做梦都在改BUG

文档图像智能分析与处理:CCIG技术论坛的思考与展望

海拥(haiyong.site)

大模型 文档图像智能处理 OCR技术

Serverless冷扩机器在压测中被击穿问题 | 京东云技术团队

京东科技开发者

Serverless GC 击穿 企业号 5 月 PK 榜 Serverless扩容

深度学习基础入门篇[10]:序列模型-词表示{One-Hot编码、Word Embedding、Word2Vec、词向量的一些有趣应用}

汀丶人工智能

人工智能 深度学习 nlp 词向量 序列模型

开源模式新探索!卡奥斯工业互联网开源开放社区再升级

Openlab_cosmoplat

开源 工业互联网 天工开物

Java开发分析用什么软件好?

真大的脸盆

Mac JAVA开发 Mac 软件 Java开发分析工具

软件测试 | FTP性能测试脚本开发(1)

测吧(北京)科技有限公司

测试

DDD在前端应用中的一些思考

阿里技术

前端 DDD

深度学习基础入门篇[9.3]:卷积算子:空洞卷积、分组卷积、可分离卷积、可变性卷积等详细讲解以及应用场景和应用实例剖析

汀丶人工智能

人工智能 深度学习 卷积网络 空洞卷积 分组卷积

SpringWeb服务构建轻量级Web技术体系:SpringHATEOAS

Java你猿哥

Java spring RESTful Web ssm

TLS 加速技术:Intel QuickAssist Technology(QAT)解决方案

vivo互联网技术

TLS 加速 Intel QAT

LDAP性能测试脚本开发

测吧(北京)科技有限公司

测试

上海国家会计学院刘梅玲:事项法会计可以有力地促进业财融合

用友BIP

业财融合 事项法会计

Java性能优化实践与策略

xfgg

Java 优化 规范

AIGC产业研究报告2023——分子发现与电路设计篇

易观分析

产业 AIGC

JavaScript作用域深度剖析:动态作用域

Immerse

面试官:你能和我说一下 CMS 垃圾回收器吗?

Java你猿哥

Java 算法 CMS 垃圾回收器 垃圾收集器

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