shapeless(2): 对函数(值)实现参数化多态

上一篇,现在来谈谈shapeless怎么实现的函数(值)的多态。参考shapeless作者原文

先回顾一下scala里函数类型的定义(带有一个参数的):

trait Function1[-T, +R] {
    def apply(t : T) : R
}

我们只要稍作修改就可以支持对入参类型支持多态,即把apply方法声明为参数化的:

trait PolyFunction1[R] {
    def apply[T](t : T) : R
}

测试一下:

scala>  object test extends PolyFunction1[String] { def apply[T](p:T) = p.toString}

scala> test("hi")
res21: String = hi

scala> test(2)
res22: String = 2

现在还差一步,怎么让返回结果类型也支持多态?虽然我们也可以在函数结尾声明返回结果是T,但那只是表示入参类型与出参类型一致的函数,并不能表达所有的函数类型。

为了达到效果,我们必须对出参类型再进行抽象!也就是说我们用高阶类型来描述(higher-kinded type,可以参考这篇:scala类型系统:24) 理解 higher-kinded-type)

对于特定类型R,它们的高阶类型为C[_],比如:

List[Int] 对应的高阶类型,即类型构造器 List

Set[String] 对应的高阶类型,即类型构造器 Set

那对于 String, Int 这样非参数化的类型,它们的高阶类型(构造器)又是什么?我们可以假定存在一个这样的类型构造器:

scala> type Id[T] = T

Id这个类型构造器与任何类型参数结合,构造出来的结果类型与参数类型一致。

scala> val s:Id[String] = "hi"
s: Id[String] = hi

scala> val s:Id[Int] = 2
s: Id[Int] = 2

scala> def f(id:Id[_]) = print("ok")
f: (id: Id[_])Unit

scala> f(2)
ok

scala> f("hello")
ok

对于String,Int这样的类型,它们的高阶类型即类型构造器Id.(其实可以把这背后看做是一个类型级别的函数,非参数化的类型其构造器都可以看做是Id这样的一个构造器类型)

现在我们重新定义多态函数PolyFunction1,通过前边高阶类型的铺垫,我们现在把结果类型R与参数类型T之间看做是一种依赖关系,即R取决于T和构造器C[_] (关于依赖类型,参考 scala类型系统:28) 依赖类型):

构造器 C[_] 与参数 T 产生结果类型 C[T] 即 R == C[T]

这样我们在定义PolyFunction1时,只需要抽象出一个结果类型的类型构造器就可以了:

trait PolyFunction1[C[_]] {
    def apply[T](t : T) : C[T]
}

现在构造一个多态函数的实例,先用一个常见的类型构造器 Set做参数,结果类型是Set[T]

scala>  object test extends PolyFunction1[Set] { def apply[T](p:T) = Set(p)}
defined module test

scala> test("hi")
res23: scala.collection.immutable.Set[String] = Set(hi)

scala> test(2)
res24: scala.collection.immutable.Set[Int] = Set(2)

现在实现了我们上一篇期望的 singletonFn 函数能够满足 T => Set[T]的效果。

再测试使用Id这个类型构造器(高阶类型)做类型参数,结果类型是Id[T],也就是T

scala>  object test extends PolyFunction1[Id] { def apply[T](p:T) = p}

scala> test("hi")
res17: String = hi

scala> test(2)
res18: Int = 2

shapeless正是通过这种思路对函数实现的多态。

shapeless(1): 从方法与函数的多态谈起

最近在用spray,它依赖了一个shapeless的库,这个库按照它github上的介绍来说,是一个基于泛型编程的提供了类型类(type class)和依赖类型(dependent type)的库。type classdependent type这两个模式术语都来自于haskell,所以新人难免会感到头大,关于type class模式,我在scala类型系统的系列文章里有提到过,可以参考这里;简单来说,它们要解决的都是多态的问题。

写这篇文章,是我阅读了shapeless作者miles的几篇blog之后的一些体会,原文在:http://www.chuusai.com/2012/04/27/shapeless-polymorphic-function-values-1/,我用的例子也引用他blog的例子。

对于多态的实现,主要有2种方式(可参考我在2013华东scala爱好者聚会时分享的ppt):

1)子类型多态
2)类型参数多态

子类型方式的多态是面向对象系统里天然支持的,我们这里讨论的主要是基于类型参数的多态。

miles的blog里提出了这样一个观点:scala里有一类的(first-class)单态(monomorphic)函数值(function value),有二类的(second-class)多态(polymorphic)方法,但没有一类的多态函数值。至少在标准的scala定义里不行。

所谓的 first-class 是表示可以像变量那样被传递,scala里方法与java里一致,不可传递,只有函数(背后是用对象承载)才可传递。我们看看为什么他说scala的函数不像方法那样支持多态(类型参数化多态)。

用他的例子,方法的多态:

scala> def singleton[T](t : T) = Set(t)

scala> singleton("foo")
res4: Set[java.lang.String] = Set(foo)

scala> singleton(23)
res5: Set[Int] = Set(23)

对方法singleton在运行时传入不同类型,编译在做类型推导时都支持的很好。

然而,当我们把它转换为函数时,类型推导就出了问题:

scala> val singletonFn = singleton _
singletonFn: (Nothing) => Set[Nothing] = <function1>

上面通过下划线把方法转为部分应用函数,但编译器却在类型推导的过程中把原先泛型参数,推导为了Nothing(关于Nothing类型,可参考这篇),或许你会奇怪,为什么原来的泛型参数T,被推导成了Nothing? 这块涉及scala类型推导的细节,参考:泛型方法转换为部分应用函数时的类型推导问题

正因为scala函数类型在构造时必须明确入参和出参类型,所以方法中的泛型参数被使用上限或下限替代(取决于协变还是逆变)。这样导致方法转为函数后,失去了原本具有的多态性:

scala> singletonFn("foo")  // 类型不匹配,要求Nothing类型

如果在对方法转为函数的时候,采用明确的类型声明:

scala> val singletonFn : String => Set[String] = singleton _

这种情况下,也只针对String类型,无法满足Int的情况。

所以miles称scala只是一类的“单态”函数,而非“多态”函数。为了实现一类的“多态”函数,他才开发了shapeless这个库,我们后续再介绍。