Scala 隐式转换之一窥

学习 Spark 的键值对 RDD 时,对其的实现比较感兴趣——它是如何让特定类型的 RDD 拥有自己独有的方法的?于是就对此进行了一些了解,发现它的本质是比较浅显但又确实非常有趣的,现在做下笔记。


当调用对象的不存在的方法,以及调用方法时传递错误类型的对象时,Scala 都会试图在隐式视图中进行一番操作来“圆场”。

如果 Scala 发现用户试图调用对象的不存在的方法,则它会试图在隐式视图中寻找构造参数为当前对象类型的隐式类,并进行隐式转换,比如我们可以通过这种方式实现类的扩展——

1
2
3
4
5
6
7
8
9
10
11
12
13
// 众所周知,String 是 final 的
object YuukiStringOps {
implicit class MyStringOps(self : String) {
// 原来的 String 只有 padTo 方法,其作用是在右侧填充字符,这里添加在左侧添加字符的方法
def padLeft(len : Int, elem : Char) : String =
self.reverse.padTo(len, elem).reverse
}
}
// 客户端使用时需要先 import 它来在作用域中引入该隐式类
object Client extends App {
import YuukiStringOps._
println("99".padLeft(4, '0')) // 0099
}

如果 Scala 发现用户给一个类型为 B 的参数传递一个类型 A 的实例,则它会试图从隐式视图中选择一个A => B的用于类型转换的函数。

1
2
3
4
5
6
7
8
9
object SomeOps {
implicit def int2String(self : Int) : String =
s"me a String: $self"
}
object Client extends App {
def printMe(str : String) : Unit = println(str)
import SomeOps._
printMe(100) // me a String: 100
}

最初的 Scala 没有隐式类,因此当时若要实现隐式类就只能通过隐式类型转换函数来实现,比如下面是不使用隐式类扩展 String 的方法——

1
2
3
4
5
6
7
8
object YuukiStringOps {
implicit def string2Ops(self : String) : YuukiStringOps_ =
new YuukiStringOps_(self)
class YuukiStringOps_(self : String) {
def padLeft(len : Int, elem : Char) : String =
self.reverse.padTo(len, elem).reverse
}
}

容易想象,这种给类添加新操作的方法翻译到 Java 中,将得到所谓的“委托”模式,但 Scala 强大之处在于,用户完全可以对包装后的类一无所知,只需要在上下文中引入该隐式对象即可,这在 Java 中是不可能的。

但是 Scala 还能做的更多!考虑一个泛型类,假如我们有这样的需求,即希望这个泛型满足特定类型的时候,让它能够调用特定的方法,这倘若放到 Java 里,就只能通过反射进行检查了,而 Scala 做得到——

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 我们想要操作的类,其方法不符合我们的需求
case class Clazz[A] (data : A) {
def describe : String = data.toString
}

object Magic {
// 我们的需求
implicit class ClazzInt(self : Clazz[Int]) {
def getValue : Int = self.data
}
}

object Client extends App {
import Magic._
val stringClazz = Clazz("hello")
// println(stringClazz.getValue) // IT DOESN'T WORK!
val intClazz = Clazz(2)
println(intClazz.getValue) // IT WORKS!
}

精巧绝伦!reduceByKey 等键值对 RDD 所特有的方法就是通过这种方式实现的。使用这种方式,既能把相关操作分离到不同地方以减少特定文件的大小,也能够避免用户对新的实现可知,统一实现的接口为 RDD;缺点则在于会让代码变得更加难懂,因此对其的使用应当是谨慎的,应在一定的模式下进行使用,期待之后的进一步学习。


也可以发现,这种操作和Haskell中的type class概念是很相近的——我们可以让类型成为一个typeclass的实例,从而让它们来具备更多方法,这种约束并非是利用接口,而是利用隐式转换,因此是可插拔的,且其能进行更多约束。这也是为什么Scala能通过像scalaz和cats等第三方库来给原有类型添加Monad等的操作。