[Scala] Implicits

作者: 达微 | 来源:发表于2019-01-05 09:26 被阅读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]的子类型。

    相关文章

      网友评论

        本文标题:[Scala] Implicits

        本文链接:https://www.haomeiwen.com/subject/nzfrrqtx.html