spark||flink||scala

[Scala] Implicits

2019-01-05  本文已影响11人  达微
 val sqlContext = new SQLContext(ssc.sparkContext)
import sqlContext.implicits._
tableRDD.map(_._2.asInstanceOf[Record]).toDF

import scala.collection.JavaConversions
value.asScala.map(_.asInstanceOf[Double])

1. Background

implicits是Scala的一种预编译特性,
编译器会根据implicit相关的规则,在程序中自动插入代码,
以修正类型错误(type error)。

例如,如果x+y不能通过类型检查,
则编译器可能会将它转化为convert(x)+y
其中,convert是一些可用的隐式转换(implicit conversion)。

如果convert可以将x转换成含有+方法的对象,
那么这种转换,就可以修复程序中的类型错误。


2. Rules for implicits

2.1 Marking Rule

Scala编译器,只使用那些显式被关键字implicit注明的函数/对象。

例如,

implicit def intToString(x: Int) = x.toString

2.2 Scope Rule

Scala编译器,只使用那些被导入到当前模块中的函数/对象,
并且,还必须是单独的标识符(single identifier)。

即,编译器不会自动添加这样的转换someVariable.convert
除非使用import someVariable.convert,将它导入为单独的标识符convert

注:
除了作用域内的函数/对象之外,
编译器还会检查伴随对象(companion object)中的implicit定义,例如,

object Dollar {
  implicit def dollarToEuro(x: Dollar): Euro = ...
}

class Dollar { ... }

其中,dollarToEuro是不用显式导入的,编译器会自己查找到它。

2.3 Non-Ambiguity Rule

如果为了修复x+y有两个不同的选择,
例如,convert1(x)+yconvert2(x)+y
那么编译器会直接报错。

为了解决这个问题,一种办法是删除一个来避免歧义,
另一种方法是,将转换方法显式的写出来,例如,convert2(x)+y

2.4 One-at-a-time Rule

编译器并不会把x+y写成convert1(convert2(x))+y
在尝试一种转换的过程中,中途并不会再次尝试其他转换。

2.5 Explicits-First Rule

编译器不会对类型良好的程序进行转换。

注:
注明为implicit的函数/对象,可以具有任意的名字。编译器是通过类型来查找合适的implicit函数/对象的,而不是通过名字。


3. Where implicits are tried

implicit会在以下三种情况中出现,
(1)对期望出现的类型进行隐式转换(implicit conversion to an expected type)
(2)对消息的接受者进行转换(converting the receiver)
(3)隐式参数(implicit parameters)

3.1 Implicit conversion to an expected type

编译器如果期望类型Y,但是只看到了类型X
就会查找implicit函数,将X转换为Y

例如,将一个Double赋值为Int就会报错,

scala> val i: Int = 3.5
<console>:11: error: type mismatch;
 found   : Double(3.5)
 required: Int
       val i: Int = 3.5
                    ^

但是,如果我们定义一个implicit函数,
Double转换为Int,程序就运行良好了,

scala> implicit def doubleToInt(x: Double) = x.toInt
doubleToInt: (x: Double)Int

scala> val i: Int = 3.5
i: Int = 3

注:
这样做并不是最佳实践,因为doubleToInt意外损失了精度,
一般而言,对于损失精度的情况最好使用显式转换。

3.2 Converting the receiver

假如我们有一个方法调用obj.doIt,可是obj并没有doIt方法,
编译器就会对obj进行隐式转换,以期结果对象拥有doIt方法。

下面我们看两个例子,
例一:Interoperating with new types

假如我们定义了一个Rational类,

class Rational(n: Int, d: Int){
  ...
  def + (that: Rational): Rational = ...
  def + (that: Int): Rational = ...
}

这个Rational类,有两个重载的+方法,
分别接受RationalInt类型的参数,
因此,我们可以将一个RationalRational相加,
还可以将一个RationalInt相加。

scala> val oneHalf = new Rational(1, 2)
oneHalf: Rational = 1/2

scala> oneHalf + oneHalf
res4: Rational  = 1/1

scala> oneHalf + 1
res5: Rational = 3/2

但是,程序在执行1 + oneHalf的时候报错了,
因为1: Int并没有一个接受Rational类型作为参数的+方法。

scala> 1 + oneHalf
<console>:6: error: overloaded method value + with
alternatives (Double)Double <and> ... cannot be applied
to (Rational)
       1 + oneHalf
         ^

为了进行这样的运算,
我们需要定义一个implicit函数,将Int转换为Rational,

scala> implicit def intToRational(x: Int) = new Rational(x, 1)
intToRational: (Int)Rational

scala> 1 + oneHalf
res6: Rational = 3/2

例二:Simulating new syntax

通过对receiver进行隐式转换,我们还可以模拟新的语法,
例如,以下表达式创建了一个Map对象,

Map(1 -> "one", 2 -> "two", 3 -> "three")

->看起来很奇怪,但它却并不是一套新的语法,
实际上,->ArrowAssoc类的一个方法,
它在scala.Predef中定义,
并且其中,还定义了一个implicit函数将Any转换为ArrowAssoc
当我们写1 -> "one"的时候,编译器会添加一个函数,
1转换成ArrowAssoc

any2ArrowAssoc(1).->("one")

其中,any2ArrowAssoc的定义如下,

package scala

object Predef {
  class ArrowAssoc[A](x: A){
    def -> [B](y: B): Tuple2[A, B] = Tuple2(x, y)
  }

  implicit def any2ArrowAssoc[A](x: A): ArrowAssoc[A] = 
    new ArrowAssoc(x)

  ...
}

这种写法称为rich wrapper模式,
它所提供的功能,仿佛扩展了Scala的语法。

3.3 Implicit parameters

柯里化(curried)函数的,最后一个参数列表,也可以设置为implicit
此时,编译器会寻找相应类型的对象,然后进行自动调用。

例如,编译器可能会将someCall(a),替换为someCall(a)(b, c, d)
这需要,someCall的最后一个参数列表被标记为implicit
还需要,bcd相应类型的对象,也被标记为implicit,且被导入。

下面我们看两个例子,
例一:

class PreferredPrompt(val preference: String) 
class PreferredDrink(val preference: String)

object Greeter {
  def greet(name: String)(implicit prompt: PreferredPrompt, 
    drink: PreferredDrink) {
    println("Welcome, "+ name +". The system is ready.") 
    print("But while you work, ") 
    println("why not enjoy a cup of "+ drink.preference +"?") 
    println(prompt.preference)
  }
}

object JoesPrefs { 
  implicit val prompt = new PreferredPrompt("Yes, master> ") 
  implicit val drink = new PreferredDrink("tea") 
}

scala> import JoesPrefs._
import JoesPrefs._

scala> Greeter.greet("Joe")(prompt, drink)    // <- 未省略
Welcome, Joe. The system is ready.
But while you work, why not enjoy a cup of tea? 
Yes, master>

scala> Greeter.greet("Joe")    // <- 已省略参数
Welcome, Joe. The system is ready.
But while you work, why not enjoy a cup of tea? 
Yes, master>

例二:

def maxListUpBound[T <: Ordered[T]](elements: List[T]): T = 
    elements match { 
        case List() => 
            throw new IllegalArgumentException("empty list!") 
        case List(x) => x 
        case x :: rest => 
            val maxRest = maxListUpBound(rest) 
            if (x > maxRest) x 
            else maxRest
}

以上我们定义了一个maxListUpBound函数,
用于获取elements: List[T]中的最大元素。

其中T <: Ordered[T]为类型参数T指定了一个upper bound
即,T必须是Ordered[T]的子类型,
否则,函数体中将无法使用>方法比较大小。

然而,这样写会有一个弊端,那就是,
对于那些内置类型,例如Int,它并没有实现为Ordered[Int]的子类型,
那么该maxListUpBound函数就不能用于elements: List[Int]了。

这个问题的一个常见解决方法如下,

def maxListImpParm[T](elements: List[T]) 
    (implicit orderer: T => Ordered[T]): T =
    elements match {
        case List() => 
            throw new IllegalArgumentException("empty list!") 
        case List(x) => x 
        case x :: rest => 
            val maxRest = maxListImpParm(rest)(orderer) 
            if (orderer(x) > maxRest) x 
            else maxRest
}

我们定义了一个类似的函数maxListImpParm
它使用了implicit parameter,它隐式传入了一个orderer函数,
用于将T类型的对象转换为Ordered[T]类型。

因此,只要T可以被转换成Ordered[T]
那么该方法就可以被使用,
无需要求TOrdered[T]的子类型。

更妙的是,
orderer实际上进行了类型转换,
而编译器找到的T => Ordered[T]类型的对象,也被注明为implicit的,
因此,orderer不仅作为implicit parameter来使用,
还可以作为隐式类型转换函数来使用,
所以,我们可以在函数体中,省略对orderer的调用,让编译器来添加。

def maxList[T](elements: List[T]) 
  (implicit orderer: T => Ordered[T]): T =
  elements match {
    case List() => 
        throw new IllegalArgumentException("empty list!") 
    case List(x) => x 
    case x :: rest => 
        val maxRest = maxList(rest)    // (orderer) is implicit 
        if (x > maxRest) x    // orderer(x) is implicit 
        else maxRest
}

结果,只有参数列表中出现了orderer,其余地方都消失了。
因此,orderer的命名是无关紧要的。

由于这种模式很常见,Scala提供了一个简洁的写法,

def maxList[T <% Ordered[T]](elements: List[T]) : T =
  elements match {
    case List() => 
        throw new IllegalArgumentException("empty list!") 
    case List(x) => x 
    case x :: rest => 
        val maxRest = maxList(rest)    // (orderer) is implicit 
        if (x > maxRest) x    // orderer(x) is implicit 
        else maxRest
}

其中,T <% Ordered[T]中的<%称为view bound
指的是,类型T的对象,可以转换成类型Ordered[T]的对象,
而且,T不必是Ordered[T]的子类型。

上一篇下一篇

猜你喜欢

热点阅读