透过Spark源码浅谈Scala隐式(implicit)机制

前言

很久没写过关于Scala的东西了(其实是不敢班门弄斧)。Scala的隐式机制在之前读Spark代码时就令人感觉fascinated,今天稍微聊聊吧。

隐式(implicit)机制是Scala的一个重要而有趣的特性,能够使Scala编程更加灵活和可扩展。我在日常编码中虽然几乎没用过它,但是在阅读某些主要用Scala写成的开源框架时,就可以说是遍地开花了。本文借助Spark中一些源码,来看看隐式机制是如何发挥作用的。

隐式参数

Scala中文官方文档中对隐式参数的解释如下:

方法可以具有隐式参数列表,由参数列表开头的implicit关键字标记。如果参数列表中的参数没有像往常一样传递,Scala将查看它是否可以获得正确类型的隐式值,如果可以,则自动传递。
Scala将查找这些参数的位置分为两类:

  • Scala在调用包含有隐式参数块的方法时,将首先查找可以直接访问的隐式定义和隐式参数 (无前缀)。
  • 然后,它在所有伴生对象中查找与隐式候选类型相关的有隐式标记的成员。

以Spark为例,RDD的许多算子都会接受一个Ordering[T]类型的隐式参数,它用于指定RDD中元素类型T的排序规则。比如takeOrdered()算子,其代码如下:

  def takeOrdered(num: Int)(implicit ord: Ordering[T]): Array[T] = withScope {
    if (num == 0) {
      Array.empty
    } else {
      val mapRDDs = mapPartitions { items =>
        // Priority keeps the largest elements, so let's reverse the ordering.
        val queue = new BoundedPriorityQueue[T](num)(ord.reverse)
        queue ++= collectionUtils.takeOrdered(items, num)(ord)
        Iterator.single(queue)
      }
      if (mapRDDs.partitions.length == 0) {
        Array.empty
      } else {
        mapRDDs.reduce { (queue1, queue2) =>
          queue1 ++= queue2
          queue1
        }.toArray.sorted(ord)
      }
    }
  }

它从RDD中按隐式参数ord规定的顺序取出前N个元素。在spark-shell中测试一下:

如果不传参数ord的话,默认就会按升序排序,那么默认排序是哪里来的呢?按照官方文档的描述,我们先在当前代码作用域中找隐式Ordering定义(没有),然后在RDD类与伴生对象中找(没有),然后在Ordering与Int中找,最后可以发现Ordering伴生对象中有如下定义:

  trait IntOrdering extends Ordering[Int] {
    def compare(x: Int, y: Int) =
      if (x < y) -1
      else if (x == y) 0
      else 1
  }
  implicit object Int extends IntOrdering

可见,编译器查找到并发挥作用的是隐式对象Int,它最终继承自Ordering[Int]特征并实现了compare()方法,其中规定了升序排序的逻辑。Ordering伴生对象中还定义了很多其他类型的隐式排序规则,除了基本类型之外,还包括String、BigDecimal甚至Option等。

如果我们不用那些已经规定好的类型,而换成自定义类型,就会报错。因为并没有自定义类型对应的隐式排序规则。例如下图:

于是我们就自己定义一个Ordering[Temp]类型的隐式值,直接定义或者新建一个Temp类的伴生对象都可以。再调用takeOrdered()算子,就可以得出正确的结果。

由此可见,隐式值、隐式对象(也包括下面的隐式方法和隐式类)的名称并没有什么要紧,重要的是类型本身。用上面的例子来说,如果我们同时定义了两个Ordering[Temp]类型的隐式值,那么编译器就会报错,因为会出现二义性,无法确定该使用哪一个了。

隐式转换

Scala中文官方文档中对隐式转换的解释如下:

一个从类型S到类型T的隐式转换由一个函数类型S => T的隐式值来定义,或者由一个可转换成所需值的隐式方法来定义。
隐式转换在两种情况下会用到:

  • 如果一个表达式e的类型为S,并且类型S不符合表达式的期望类型T。
  • 在一个类型为S的实例对象e中调用e.m,如果被调用的m并没有在类型S中声明。

在第一种情况下,搜索转换c,它适用于e,并且结果类型为T。在第二种情况下,搜索转换c,它适用于e,其结果包含名为m的成员。

这个解释有点过于文绉绉了,下面仍然通过例子来说明。

我们经常会对RDD(准确来说是Pair RDD,即RDD[(K,V)])使用诸如rdd.aggregateByKey()、rdd.reduceByKey()、rdd.join()这类算子,但它们在RDD抽象类本身并没有定义,在RDD类的所有子类中也同样找不到。这是因为在RDD类的伴生对象中,有如下的隐式转换方法。

  implicit def rddToPairRDDFunctions[K, V](rdd: RDD[(K, V)])
    (implicit kt: ClassTag[K], vt: ClassTag[V], ord: Ordering[K] = null): PairRDDFunctions[K, V] = {
    new PairRDDFunctions(rdd)
  }

它的作用是将RDD[(K,V)]类型隐式转换成PairRDDFunctions类型,而PairRDDFunctions类中定义了这些算子。

可见,这适用于官方文档中说明的第二种情况。假设我们执行rdd.reduceByKey(),由于RDD类没有定义该算子,编译器就会按顺序寻找源类型为RDD[(K,V)]的隐式转换,并最终找到隐式转换方法rddToPairRDDFunctions()中,目标类型PairRDDFunctions定义了reduceByKey()算子,执行成功。

那么第一种情况呢?在SparkContext.scala中有个WritableConverter类,其伴生对象中有很多这样的隐式值定义:

  implicit val intWritableConverterFn: () => WritableConverter[Int] =
    () => simpleWritableConverter[Int, IntWritable](_.get)
  implicit val longWritableConverterFn: () => WritableConverter[Long] =
    () => simpleWritableConverter[Long, LongWritable](_.get)
  implicit val doubleWritableConverterFn: () => WritableConverter[Double] =
    () => simpleWritableConverter[Double, DoubleWritable](_.get)
  implicit val floatWritableConverterFn: () => WritableConverter[Float] =
    () => simpleWritableConverter[Float, FloatWritable](_.get)
  implicit val booleanWritableConverterFn: () => WritableConverter[Boolean] =
    () => simpleWritableConverter[Boolean, BooleanWritable](_.get)

它们的作用都是隐式地将Hadoop I/O体系中的Writable数据类型转化为基本数据类型,更贴近于我们平常理解的类型转换了。

同样地,隐式转换也不能存在二义性(不能定义两个源类型和目标类型都相同的转换),并且转换是单向进行的。

隐式类

隐式类是Scala 2.10开始引入的新特性,关于它的说明,可以参考https://docs.scala-lang.org/zh-cn/overviews/core/implicit-classes.html。由于它的历史并不算长,因此Spark中用到的也并不多。在其通用工具类Utils中有一个,名为Lock,如下:

  private implicit class Lock(lock: LockInfo) {
    def lockString: String = {
      lock match {
        case monitor: MonitorInfo =>
          s"Monitor(${lock.getClassName}@${lock.getIdentityHashCode}})"
        case _ =>
          s"Lock(${lock.getClassName}@${lock.getIdentityHashCode}})"
      }
    }
  }

这样就相当于给LockInfo类新增了一个方法lockString(),用来以字符串形式打印出锁信息。在Utils类中就有了以下这样的调用:

val heldLocks = (threadInfo.getLockedSynchronizers ++ threadInfo.getLockedMonitors).map(_.lockString).toSet

可见,隐式类可以方便地对现有的类进行扩展。

如何寻找隐式定义

在Scala官网的一篇文章https://docs.scala-lang.org/tutorials/FAQ/finding-implicits.html中,详细讲述了Scala是怎样在代码中寻找隐式定义的,简要总结如下:

  • 在当前代码作用域中寻找隐式定义(隐式值/方法/对象/类),如果找得到,即应用之。
  • 若在当前代码作用域没有找到,就检查与隐式类型T相关的以下所有范围:
    • T的伴生对象;
    • T继承或实现的类、特征等的伴生对象;
    • 包含T的参数化类型的伴生对象(即如果是S[T]类型,也要检查S的伴生对象);
    • T的外部类的伴生对象(仅当T是内部类时)。

The End

民那晚安。祝身体健康,百毒不侵。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容