Scala implicit, view bounds和context bounds

故事起因是从我看不懂<:, >:, <%, <:<, =:=, <%<这些符号开始的。本文介绍了scala中implicit,View Bounds 和Context Bounds的用法。并且对type class的引入做了铺垫。

Intro: Implicit

先介绍一下Implicit的4种用法,本文后半部分和type class的都会用到。主要节选自Scala之——Implicit 详解

隐式类型转换

隐式类型转换用于在参数类型不匹配的时候进行隐式转换

import scala.language.implicitConversions

// 定义猫和狗
class Cat(val name: String) {
def sayhi() = println(s"$name says Meow!")
}
class Dog(val name: String) {
def sayhi() = println(s"$name says Woof!")
}

//定义隐式类型转换函数
implicit def dogToCat(x: Dog): Cat = new Cat(x.name)

//定义提醒铲屎官喂食的函数
def remind(cat: Cat) = cat.sayhi()

//现在狗狗也可以提醒铲屎官喂食了, 虽然它变成了猫只能喵喵叫
val ein = new Dog("Ein")
remind(ein)
scala> Ein says Meow!

隐式类

隐式类可以在某个实例找不到方法的时候“悄悄”变成另一个类调用方法。

//定义一个矩形类
case class Rectangle(width: Int, height: Int)

//定义一个隐式类
implicit class RectangleMakerOp(width: Int) {
def x(height: Int) = Rectangle(width, height)
}

//编译器自动生成了以下函数
implicit def RectangleMakerOp(width: Int) = new RectangleMakerOp(width)

//可以直接使用x来创建实例了, 3是Int, 找不到x方法, 通过隐式变成RectangleMakerOp类实现了x
scala> val easyRec = 3 x 4
easyRec: Rectangle = Rectangle(3,4)

隐式参数

隐式参数就是通过import从别的域导入,不需要显式地调用就可以使用的参数

//声明一下薮猫,小包和boss
case class Serval(name: String)
case class Kaban(name: String)
case class Boss(name: String)

//欢迎小包来到加帕里
object Japari {
def welcomeToJapariPark(kaban: Kaban)(implicit serval: Serval, boss: Boss) = {
println(s"Welcome ${kaban.name}, here is Japari, we are ${serval.name} and ${boss.name}")}
}

//薮猫和boss隐藏在暗处. 注意Case Class不需要new关键字
object CuteCouple {
implicit val serval = Serval("傻八路")
implicit val boss = Boss("BOSS")
}

//欢迎新客人,从暗中出现!
object Welcome extends App{
import CuteCouple._
val kaban = Kaban("小包")
Japari.welcomeToJapariPark(kaban)
}

scala> Welcome 小包, here is Japari, we are 傻八路 and BOSS

上下文绑定(Context bound)

简单说上下文绑定的语法糖是因为Scala标准库中提供了一个方法,让编译器可以自己寻找到一个类的隐式变量:

def implicitly[T](implicit t: T) = t

所以才可以省掉隐式参数直接使用上下文绑定的语法。具体什么是上下文绑定,请看下文。

Type Bounds

为了理解一下<:, >:, <%这三个符号代表的含义,参考了Identify and describe Scala’s generic type constraints中的阐述:

首先是type bounds:

  • <: - uppper type bound, S <: T means that S is a subtype of T
  • >: - lower type bound, S >: Tmeans that S is a supertype of T

这和Java中的super, extends一致,事实上它们会被编译成相同的字节码。

其次view bound和context bound是语法糖:

  • <% - view bound, S <% T expresses that S must come equipped with a view that maps its values into values of type T
  • : - context bound,

而这些不会被编码为Java可以理解的内容。 (尽管它们会表示为 scala signature, 一种scala为了辅助编译器而对所有类添加的标识,它们最终组成了Scala reflection library的基础)

这两个语法糖实际上都会被转换成隐式参数:

// view bound
def fn[A <% B](arg: A) = ... //语法糖
def fn[A](arg: A)(implicit ev: A => B) = ... //去掉语法糖
//context bound
def fn[A : Numeric](arg: A) = ... //语法糖
def fn[A](arg: A)(implicit ev: Numeric[A]) = ... //去掉语法糖

View Bounds & Context Bounds

为了更好地理解上一节后两种语法,在What are Scala context and view bounds?中进一步得到了具体阐述。

什么是View Bound?

view bound 是Scala中的一种机制,使得能够将某一类型A当作另一类型B来使用。典型的用法如下:

def f[A <% B](a: A) = a.bMethod

换句话说, A 需要拥有隐式转换为B的方法, 这样才能够在A的对象中调用B的方法。在Scala标准库中(2.8.0之前)最普遍的用法是和Ordered一起使用:

def f[A <% Ordered[A]](a: A, b: A) = if (a < b) a else b

由于我们可以将A 转换为Ordered[A], 并且Ordered[A] 定义了<(other: A): Boolean, 因此我们才可以使用a < b这样的表达式。

请注意view bounds are deprecated, 以后别用了。

什么是Context Bound?

Context bounds 在Scala 2.8.0中被引入, 是专门与所谓的 type class pattern 一起使用的, 尽管没那么简洁,但也还是实现了Haskell中type classes所提供的功能。

虽然view bound 可以被用于简单类型(例如A <% String), 但context bound 需要一个参数化的类型(parameterized type) , 例如上一节例子中的Ordered[A] , 而不是String这样的简单类.

与view bound中描述了一种隐式的转换 (conversion)不同,context bound 描述了一种隐式的(value)。它被用于声明对于某些类型 A, 存在一个类型为B[A]的隐式值可供使用。 例子如下:

def f[A : B](a: A) = g(a) // 这里g需要一个类型为B[A]的隐式的值

由于我们不知道如何立即使用这个特性,因此它比view bound更加令人困惑。Scala中最普遍的用法是这样:

def f[A : ClassManifest](n: Int) = new Array[A](n)

因为一些关于type erasure的神秘原因,初始化一个元素为参数化的类型的Array需要提供一个ClassManifest。具体Manifest做了什么请看最后一节。

另一个非常普遍的用法稍微复杂一些:

def f[A : Ordering](a: A, b: A) = implicitly[Ordering[A]].compare(a, b)

implicitly在这里被用作取得我们需要的隐式值,即一个类型为Ordering[A]的值,它定义了compare(a: A, b: A): Int这个方法。

接下来我们会使用另一种方法实现这个特性.

View Bounds 和Context Bounds是如何实现的?

也许这并不令人惊讶,view bounds和context bounds都是通过预先给出定义的隐式参数实现的。事实上以上例子都是语法糖包装之后的结果,接下来我们去掉语法糖看看:

// view bounds
def f[A <% B](a: A) = a.bMethod //语法糖
def f[A](a: A)(implicit ev: A => B) = a.bMethod //去掉语法糖
// context bounds
def g[A : B](a: A) = h(a) //语法糖
def g[A](a: A)(implicit ev: B[A]) = h(a) //去掉语法糖

所以自然而然地,上一节末尾的例子通过完整的语法来编写就是:

def f[A](a: A, b: A)(implicit ord: Ordering[A]) = ord.compare(a, b)

View Bounds是用来做什么的?

View bounds 被应用在 pimp my library 设计模式中,即:希望对已有类“添加”方法,但依然希望返回原类型的情形。如果你不需要返回原类型,那你就不需要view bound。

View bound最经典的用法是处理Ordered。取上一小节相同的例子,如果令泛型为Int,注意到Int并不是具备<方法的Ordered(尽管事实上是有隐式转换的),但函数返回值要求是Int,此时就需要一个view bound:

def f[A <% Ordered[A]](a: A, b: A): A = if (a < b) a else b

如果没有view bounds,这个例子就不能运行。然而,如果我们返回其他类型,那就不需要view bound了:

def f[A](a: Ordered[A], b: A): Boolean = a < b

这里的转换(如果有的话) 发生在传递参数给f之前,因此f并不需要知道这些。

除了Ordered之外,最常用的情况还有处理属于Java Classes的StringArray的时候, 可以把它们当成Scala collections来看。例如:

def f[CC <% Traversable[_]](a: CC, b: CC): CC = if (a.size < b.size) a else b

如果不用view bounds的话, String的返回类型会变成WrappedString(Scala 2.8),Array也同理。

就算这个类只是被用作返回类型的类型参数,相同的事情还是会发生:

def f[A <% Ordered[A]](xs: A*): Seq[A] = xs.toSeq.sorted

Context Bounds是用来做什么的?

Context bounds 被用在 typeclass pattern 中,也就是Haskell中的type classes。 简单来说,这个设计模式通过一系列隐式适应从功能上实现了继承的一种替代方案。

经典的例子是Scala 2.8中的Ordering,它在Scala Library中彻底替代了Ordered。用法是:

def f[A : Ordering](a: A, b: A) = if (implicitly[Ordering[A]].lt(a, b)) a else b

当然,以下写法更经常被看到:

def f[A](a: A, b: A)(implicit ord: Ordering[A]) = {
import ord.mkOrderingOps
if (a < b) a else b
}

这里利用了Ordering中的一些隐式转换,可以直接使用传统的操作符风格a < b而不是lt(a, b)。Scala 2.8中的另一个例子是Numeric:

def f[A : Numeric](a: A, b: A) = implicitly[Numeric[A]].plus(a, b)

一个更复杂的例子是CanBuildFrom的集合用法,不过限于篇幅就不介绍了。除此之外还有ClassManifest的神秘用法,需要在不知道具体类型的情况下初始化数组时用到。

在typeclass中需要的context bound更常见于自己设计的类中,因为它实现了问题分解,而view bounds一般可以通过好的设计来规避,因此它一般是用来解决别人的代码设计问题的。

尽管context bounds可能已经存在很久了,2010年开始它才真正发挥作用,在Scala中的核心库和框架中都得到了应用。最典型的例子是Scalaz,它将Haskell中的很多优点引入了Scala。

Generalized Type Constraints

有了上述背景,接下来再来理解一下<:<, =:=, <%<这三个符号代表的含义。What do <:<, <%<, and =:= mean in Scala 2.8, and where are they documented?这个问题的回答阐述得比较清楚。

这些都被称为generalized type constraints。他们允许你从一个类型参数化(type-parameterized)的class或者trait中进一步限制它的类型参数。这里有个例子:

case class Foo[A](a:A) { // 'A' 可以是任何类型
// getStringLength can only be used if this is a Foo[String]
def getStringLength(implicit evidence: A =:= String) = a.length
}

只有AString的情况下,隐式参数evidence才会由编译器提供。你可以认为这是一个AString的证明–参数本身并不重要,只需要知道它存在就行了(好吧,技术上说这还是挺重要的。因为它代表了一个把A变成String的隐式类型转换,有了它才允许你调用a.length的时候编译器不会抱怨)

现在我可以这么用了:

scala> Foo("blah").getStringLength
res6: Int = 4

但我要是用在包含除了String的其他类型的Foo对象上面,就不可以:

scala> Foo(123).getStringLength
<console>:9: error: could not find implicit value for parameter evidence: =:=[Int,String]

你可以认为这个error在说“我找不到Int == String的迹象,可函数说好是这样的!” 比起Foo的要求,getStringLength()对类型A添加了更多限制。也就是说只能在Foo[String]上调用getStringLength。这一限制是在编译期就实现的,which is cool!

<:<<%< 有相似的功能,但略微不同:

  • A =:= B 表示A和B是严格相等的类
  • A <:< B 表示A是B的subtype (和之前的type bounds<:类似)
  • A <%< B means A must be viewable as B, possibly via implicit conversion (analogous to the simple type constraint <%)

假设使用这一特性定义一个List.sumInts方法,可以对一组整数求和,你不希望这个方法被任何其他List调用,只允许List[Int]调用。然而List的类型构造函数没有办法添加这样具体的限制,并且我们也确实需要由string, foo, bar等等其他类型组成的List,那么就可以对sumInts使用我们的generalized type constraint,通过它来保证只有List[Int]才能调用这个方法。这对需要针对具体类型编写特殊需求的代码非常实用。

Manifest

有了以上知识,下面解释一下Manifest的概念。节选自What is a Manifest in Scala and when do you need it?

如何实现创造一个泛型数组的过程呢?和Java不同,Scala允许实例化一个泛型数组Array[T],此时T是一个类型参数。在不知道类型并且Java也不支持的情况下如何实现这一特性呢?

唯一的办法只能是在runtime中要求提供更多关于类型T的信息。Scala2.8拥有了一个叫做Manifest的功能实现了这个机制。一个类型为Manifest[T]的实例提供了关于类型T的所有需要的信息。Manifest的值通常通过隐式参数提供,编译器知道如何通过已知的类型T来构造它们。

同时还有一个弱一点的形式ClassManifest,可以在只知道类型参数的基类而不知道具体是哪一类型的的情况下进行构造。

这样的信息才提供了数组构造的时候需要的runtime信息。


另一个回答举了之前用到的例子,懒得翻译了。

The compiler knows more information about types than the JVM runtime can easily represent. A Manifest is a way for the compiler to send an inter-dimensional message to the code at runtime about the type information that was lost.

One common use of Manifests is to have your code behave differently based on the static type of a collection. For example, what if you wanted to treat a List[String] differently from other types of a List:

def foo[T](x: List[T])(implicit m: Manifest[T]) = {
if (m <:< manifest[String])
println("Hey, this list is full of strings")
else
println("Non-stringy list")
}

foo(List("one", "two")) // Hey, this list is full of strings
foo(List(1, 2)) // Non-stringy list
foo(List("one", 2)) // Non-stringy list

A reflection-based solution to this would probably involve inspecting each element of the list.

A context bound seems most suited to using type-classes in scala, and is well explained here by Debasish Ghosh: http://debasishg.blogspot.com/2010/06/scala-implicits-type-classes-here-i.html

Context bounds can also just make the method signatures more readable. For example, the above function could be re-written using context bounds like so:

def foo[T: Manifest](x: List[T]) = {
if (manifest[T] <:< manifest[String])
println("Hey, this list is full of strings")
else
println("Non-stringy list")
}

小结

总结一下View bound和Context Bound。

View bound中描述了一种隐式的转换

def f[A <% B](a: A): A = a.bMethod
def f[A](a: A)(implicit ev: A => B) = a.bMethod

这两段等价代码告诉编译器对于某个类A,如果找不到bMethod方法,可以通过寻找隐式类型转换将其当成类B来使用。但是函数返回的依然是类A

Context bound 描述了一种隐式的(value)。

def f[A : Numeric](a: A, b: A) = implicitly[Numeric[A]].plus(a, b)
def f[A](a: A, b: A)(implicit ev = Numeric[A]) = ev.plus(a, b)

这两段等价代码告诉编译器对于某些类型 A,如果找不到plus方法,存在一个类型为Numeric[A]的隐式值可供使用。而Numeric就是一个Type Class。