第七章 对HList进行函数化操作
普通的Scala程序大量使用像map和flatMap这样的函数化操作。学习到此我们脑子中不免会产生一个问题,能否对HList实例执行相同的函数化操作呢?尽管我们必须做一些与普通的Scala函数化操作稍微不同的事情,但是答案是肯定的,可以使用类型类的技巧并且shapeless提供了一套ops类型类来帮助我们实现这一功能。
在我们深入学习这些类型类之前,我们需要先学习一下shapeless中如何定义多态函数(polymorphic function),多态函数能够实现对异构数据的map操作。
7.1 动机:对HList进行映射(map)操作
我们通过学习map方法的原理来引出多态函数。图7.1展示了普通list对象的映射操作。如果我们有一个List[A]对象,并提供一个“A=>B”的函数,据此可得List[B]对象。

HList对象元素的异构类型导致这一方式不能正常运行,Scala函数修复了输入和输出类型使得map的结果的每一个元素都必须拥有相同的类型。
理想情况下我们需要一个像图7.2中的map操作,它判断每一个输入的类型并决定每一个输出的类型,最终得到一个封闭的、能保持HList异构本质的组合变换。

不幸的是我们不能使用Scala函数来实现这种操作,需要一些新的方式,这种方式就是多态函数。
7.2 多态函数(polymorphic function)
shapeless为实现多态函数提供了一个叫做Poly的类型。简单解释多态函数如何工作即为其结果类型依赖参数类型。注意下一节中不包含真实的shapeless代码——为了灵活性和易用性我们省略了很多不重要的代码。下面结合实际的shapeless中的Poly类来创建一个简单的API以展示我们实现函数化操作的目的。
7.2.1 Poly如何工作
Poly类的核心代码是一个泛型的apply方法,它除了有一个普通A类型的参数,还接受一个Case[P, A](原文为Case[A])类型的隐式参数。Case和Poly的定义如下:
当我们实现一个实际的Poly类的时候,需要为每一个关心的参数类型提供Case实例,Case实例的apply方法定义了对此种类型的数据做何种映射。下面代码实现了实际的函数体:
当我们调用myPoly.apply时,编译器搜索对应的隐式Case实例并调用它。例如下述代码会调用隐式的intCase:
我们可以使用一些微妙的作用域技巧,使编译器能够自动定位Case实例而不用任何多余的引入。Case有一个额外的类型参数P,该参数引用Poly的单例类型。Case[P, A]的隐式作用域包括Case、P和A的伴随对象。我们把P设置为myPoly.type,myPoly.type的伴随对象就是myPoly自身。换句话说,不管在哪里调用myPoly.apply方法Poly里定义的隐式Case实例总是处在作用域内。
7.2.2 Poly语法规则
上面的代码不是实际的shapeless代码,但是幸运的是,shapeless提供了更加容易的Poly实例定义方式。下面是以正确的语法定义的myPoly:
此处与之前定义的样例语法有几个关键的不同之处:
我们继承自Poly1特质而不是Poly,shapeless提供了Poly类型和其一系列的子类,从Poly1到Poly22,它们支持不同参数个数的多态函数。
Case.Aux类型与Poly的单例类型看上去没有关联,Case.Aux是Poly1里定义的一个类型别名,其实二者是有关联的,在Poly1的定义中我们可以清晰的看到。
我们使用了一个名叫at的辅助方法来定义case隐式实例,这与3.1.2节中介绍的实例构造函数的作用相同,可以减少冗余代码。
除了语法差异,使用shapeless真实代码定义的myPoly在功能上与样例中定义的myPoly是一致的,我们能给其提供一个Int或String类型的参数,得到一个相应类型的返回结果。具体如下:
shapeless同样支持多个参数的Poly,下面是两个参数的例子:
因为Case实例只是隐式值,我们能基于类型类定义Case实例并实现在前面章节介绍过的所有高级隐式解析。下面是不同上下文环境下的求数字之和的简单的例子:
类型推断特质
Poly将Scala的类型推断移出了编译器的舒适区,只需要让编译器一次多做几个类型推断就能很容易迷惑它。比如下面的代码能够正常编译:
然而,将上述两行代码组合成一行就会报错。如下:
如果我们增加一个类型注释,编译正常。如下:
这种行为很让人困惑和讨厌,也并没有固定的规则能让我们避免这一问题,唯一的方法就是试着不要过度约束编译器,一次只解决一个约束并在编译器报错的时候补充一点提示信息。
7.3 使用Poly完成map和flatMap操作
shapeless提供了一套基于Poly的函数化操作,每一个都是作为一个ops类型类来实现的。此处我们以map和flatMap操作为例。下面是map操作的代码:
注意结果HList的元素类型与sizeOf里的Case实例的输出类型相匹配。只需为HList实例准备一个Poly对象,在此Poly对象中对该HList的所有类型都提供相应的Case实例,就能对该HList实例调用map函数。但是如果编译器不能为某个成员找到其对应的Case实例那么就会报错。如下:
我们也能对HList实例进行flatMap操作,只要在定义的Poly实例中使每一个Case实例返回的是HList类型即可。代码如下:
再次强调,如果调用flatMap的HList实例有某个元素类型所对应的Case实例没有定义或者其对应的Case实例返回的结果不是HList类型那么编译器就会报错。如下:
map和flatMap分别基于Mapper和FlatMapper类型类,我们将在7.5节中看到一个直接使用Mapper进行操作的例子。
7.4 使用Poly完成Fold操作
除了map和flatMap之外,shapeless还提供了基于Poly2的foldLeft和foldRight操作,其区别在于Case实例需要对两个变量进行处理。代码如下:
同样还能完成reduceLeft、reduceRight、foldMap等操作,每一个操作都有与之相对应的类型类,我们将把研究这些可用的操作作为练习留给读者。
7.5 使用Poly定义类型类
我们能以Poly和像Mapper、FlatMapper一样的类型类为基础,定义我们自己的类型类。作为例子,我们定义一个实现从一个样例类到另一个样例类的map操作的类型类ProductMapper。代码如下:
我们能用一个Mapper参数和一对Generic参数来创建一个ProductMapper的实例。代码如下:
有趣的是,尽管定义了一个Poly类型的类型参数P,但是在我们的代码的任何位置均不涉及类型P的值。Mapper类型类使用隐式解析来寻找Case实例,所以编译器只需要知道P的单例类型就能从其中加载定义好的相关Case实例。
我们来创建一个扩展方法以使ProductMapper更容易被调用,用户在调用的时候只需要指定B的类型。可以使用一些间接的方式来让编译器从值参数推导Poly的类型。此处定义了一个Builder类,并为该类提供一个泛型的apply方法,传入一个poly变量和一个隐式的ProductMapper对象,这样就能自动的根据B的类型推断出P的类型。代码如下:
下面是上述方法的使用样例:
mapTo语法看上去像一个单一的方法调用,但实际上是两次:一次调用mapTo确定B类型参数,另一次调用Builder.apply方法来指定Poly的类型。一些shapeless的内置的ops扩展方法使用相似的技巧为用户提供方便。
7.6 小结
这一章我们讨论了多态函数,它的返回类型基于参数类型的变化而变化。我们看到了shapeless的Poly类型是如何定义的以及如何用它来实现像map、flatMap、foldLeft和foldRight这样的函数化操作。
每一个对HList实例的操作都是作为扩展方法而实现的,并基于对应的类型类:Mapper、FlatMapper、LeftFolder等等。我们能使用这些类型类、Poly类型和在4.3节中介绍的技巧来创建我们自己的类型类,它涉及复杂的转换过程,最终达到我们想要的效果。
最后更新于
这有帮助吗?