[Scala] Implicits

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]的子类型。


参考

Programming in Scala - Chapter 21

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

推荐阅读更多精彩内容