Scala在哪里寻找隐式函数?

对于Scala新手来说,一个隐式的问题似乎是:编译器在哪里寻找隐式函数?我指的是含蓄,因为这个问题似乎从来没有完全形成过,好像没有词来形容它。:-)例如,下面的integral的值来自哪里?

scala> import scala.math._
import scala.math._


scala> def foo[T](t: T)(implicit integral: Integral[T]) {println(integral)}
foo: [T](t: T)(implicit integral: scala.math.Integral[T])Unit


scala> foo(0)
scala.math.Numeric$IntIsIntegral$@3dbea611


scala> foo(0L)
scala.math.Numeric$LongIsIntegral$@48c610af

对于那些决定学习第一个问题答案的人来说,另一个问题是,在某些明显模棱两可的情况下(但无论如何都要编译),编译器如何选择使用哪个隐式?

例如,scala.Predef定义了两个从String的转换:一个到WrappedString,另一个到StringOps。然而,这两个类共享许多方法,那么为什么Scala在调用map时不抱怨歧义呢?

注意:这个问题是受到另一个问题的启发,希望以更一般的方式说明问题。示例是从那里复制的,因为答案中引用了它。

53350 次浏览

隐式的类型

在Scala中,隐式指的是可以“自动”传递的值,也就是说,从一种类型自动转换为另一种类型。

隐式转换

简单地说一下后一种类型,如果在类C的对象o上调用方法m,而该类不支持方法m,那么Scala将寻找从C支持m的隐式转换。一个简单的例子是String上的map方法:

"abc".map(_.toInt)

String不支持方法map,但是StringOps支持,并且有一个从StringStringOps的隐式转换可用(参见Predef上的implicit def augmentString)。

隐式参数

另一种隐式是隐式参数。这些参数像其他参数一样传递给方法调用,但编译器会尝试自动填充它们。如果做不到,它就会抱怨。一个可以显式地传递这些参数,这就是如何使用breakOut,例如(参见关于breakOut的问题,在你感觉有挑战的一天)。

在这种情况下,必须声明隐式方法的需要,例如foo方法声明:

def foo[T](t: T)(implicit integral: Integral[T]) {println(integral)}

视图边界

有一种情况,隐式既是隐式转换,也是隐式形参。例如:

def getIndex[T, CC](seq: CC, value: T)(implicit conv: CC => Seq[T]) = seq.indexOf(value)


getIndex("abc", 'a')

方法getIndex可以接收任何对象,只要存在从其类到Seq[T]的隐式转换可用。因此,我可以将String传递给getIndex,它将工作。

在幕后,编译器将seq.IndexOf(value)更改为conv(seq).indexOf(value)

这是非常有用的,有语法糖来写它们。使用这个语法糖,getIndex可以这样定义:

def getIndex[T, CC <% Seq[T]](seq: CC, value: T) = seq.indexOf(value)

这个语法糖被描述为视图绑定,类似于上界 (CC <: Seq[Int])或下界 (T >: Null)。

上下文范围

隐式参数的另一种常见模式是类型类模式。此模式允许向没有声明公共接口的类提供公共接口。它既可以用作桥接模式(获得关注点分离),也可以用作适配器模式。

你提到的Integral类是类型类模式的一个经典例子。Scala标准库中的另一个例子是Ordering。有一个库大量使用这种模式,称为Scalaz。

下面是它使用的一个例子:

def sum[T](list: List[T])(implicit integral: Integral[T]): T = {
import integral._   // get the implicits in question into scope
list.foldLeft(integral.zero)(_ + _)
}

它还有一个语法糖,称为上下文绑定,由于需要引用隐式变量,它就不那么有用了。该方法的直接转换如下所示:

def sum[T : Integral](list: List[T]): T = {
val integral = implicitly[Integral[T]]
import integral._   // get the implicits in question into scope
list.foldLeft(integral.zero)(_ + _)
}

当你只需要通过上下文边界给其他使用它们的方法时,上下文边界更有用。例如,Seq上的方法sorted需要隐式Ordering。要创建方法reverseSort,可以这样写:

def reverseSort[T : Ordering](seq: Seq[T]) = seq.sorted.reverse

因为Ordering[T]被隐式传递给reverseSort,所以它可以隐式传递给sorted

隐式是怎么来的?

当编译器看到需要隐式参数时,无论是因为你调用的方法在对象的类中不存在,还是因为你调用的方法需要隐式参数,它都会搜索一个符合需要的隐式参数。

这个搜索遵循某些规则,这些规则定义了哪些隐式是可见的,哪些是不可见的。下表显示了编译器搜索隐式的位置,摘自Josh Suereth关于隐式的优秀演讲(时间戳20:20),我衷心推荐给任何想要提高Scala知识的人。从那时起,它得到了反馈和更新。

下面数字1下可用的隐式比数字2下可用的隐式优先。除此之外,如果有几个符合隐式形参类型的参数,将使用静态重载解析规则选择一个最具体的参数(参见Scala规范§6.26.3)。更详细的信息可以在答案后面的一个问题中找到。

  1. 首先查看当前范围
    • 在当前作用域中定义的隐式
    • 明确的进口
    • 通配符进口
    • 其他文件的作用域相同
  2. 现在查看关联的类型
    • 类型的伴生对象
    • 参数类型(2.9.1)的隐式作用域
    • 类型参数(2.8.0)的隐式作用域
    • 嵌套类型的外部对象
    • 其他维度

让我们给他们举几个例子:

在当前作用域中定义的隐式

implicit val n: Int = 5
def add(x: Int)(implicit y: Int) = x + y
add(5) // takes n from the current scope

明确的进口

import scala.collection.JavaConversions.mapAsScalaMap
def env = System.getenv() // Java map
val term = env("TERM")    // implicit conversion from Java Map to Scala Map

通配符进口

def sum[T : Integral](list: List[T]): T = {
val integral = implicitly[Integral[T]]
import integral._   // get the implicits in question into scope
list.foldLeft(integral.zero)(_ + _)
}

在其他文件中相同的范围

编辑:这似乎没有不同的优先级。如果你有一些例子,说明优先级的区别,请作出评论。否则,不要依赖这个。

这类似于第一个示例,但假设隐式定义位于与其用法不同的文件中。还可以查看方案对象如何用于引入隐式。

类型的伴生对象

这里有两个值得注意的对象同伴。首先,“源”的宾语伴语;类型被调查。例如,在对象Option内部有一个到Iterable的隐式转换,因此可以在Option上调用Iterable方法,或者将Option传递给需要Iterable的对象。例如:

for {
x <- List(1, 2, 3)
y <- Some('x')
} yield (x, y)

该表达式由编译器翻译为

List(1, 2, 3).flatMap(x => Some('x').map(y => (x, y)))

然而,List.flatMap需要TraversableOnce,而Option不是。然后,编译器查看Option的对象伙伴,并找到到Iterable的转换,这是一个TraversableOnce,使得这个表达式正确。

第二,期望类型的伴生对象:

List(1, 2, 3).sorted

方法sorted接受隐式Ordering。在这种情况下,它在对象Ordering(类Ordering的伴侣)内部查找,并在那里找到一个隐式的Ordering[Int]

注意,还将研究超类的伴生对象。例如:

class A(val n: Int)
object A {
implicit def str(a: A) = "A: %d" format a.n
}
class B(val x: Int, y: Int) extends A(y)
val b = new B(5, 2)
val s: String = b  // s == "A: 2"

这就是Scala如何在你的问题中找到隐式Numeric[Int]Numeric[Long]的,顺便说一下,因为它们在Numeric中,而不是Integral中。

参数类型的隐式作用域

如果你有一个参数类型为A的方法,那么类型A的隐式作用域也会被考虑。通过“隐式范围”;我的意思是,所有这些规则都将被递归地应用——例如,根据上面的规则,将搜索A的伴生对象以寻找隐式。

注意,这并不意味着将搜索A的隐式作用域以查找该形参的转换,而是整个表达式。例如:

class A(val n: Int) {
def +(other: A) = new A(n + other.n)
}
object A {
implicit def fromInt(n: Int) = new A(n)
}


// This becomes possible:
1 + new A(1)
// because it is converted into this:
A.fromInt(1) + new A(1)

这是从Scala 2.9.1开始提供的。

类型参数的隐式作用域

这是使类型类模式真正工作所必需的。例如,考虑Ordering:它在它的伴生对象中带有一些隐式,但你不能向它添加东西。那么如何为自己的类创建一个自动找到的Ordering呢?

让我们从实现开始:

class A(val n: Int)
object A {
implicit val ord = new Ordering[A] {
def compare(x: A, y: A) = implicitly[Ordering[Int]].compare(x.n, y.n)
}
}

所以,考虑一下你打电话的时候会发生什么

List(new A(5), new A(2)).sorted

正如我们所看到的,sorted方法需要Ordering[A](实际上,它需要Ordering[B],其中B >: A)。在Ordering中没有任何这样的东西,并且没有"source"键入要查看的内容。显然,它在A中找到它,而AOrdering类型参数

这也是各种期望CanBuildFrom的收集方法的工作方式:隐式在CanBuildFrom类型参数的伴生对象中找到。

请注意: Ordering被定义为trait Ordering[T],其中T是一个类型参数。在前面,我说过Scala在类型参数内部进行检查,这没什么意义。上面隐式查找的是Ordering[A],其中A是一个实际类型,而不是类型参数:它是从类型参数Ordering。请参见Scala规范的7.2节。

这是从Scala 2.8.0开始提供的。

嵌套类型的外层对象

我还没见过这样的例子。如果有人能和我分享,我会很感激的。原理很简单:

class A(val n: Int) {
class B(val m: Int) { require(m < n) }
}
object A {
implicit def bToString(b: A#B) = "B: %d" format b.m
}
val a = new A(5)
val b = new a.B(3)
val s: String = b  // s == "B: 3"

其他维度

我很确定这是一个笑话,但这个答案可能不是最新的。因此,不要把这个问题作为正在发生的事情的最终仲裁者,如果你注意到它已经过时了,请通知我,以便我可以修复它。

编辑

相关问题:

我想找出隐式参数解析的优先级,而不仅仅是它在哪里查找,所以我写了一篇博客文章重访意味着没有进口税(并在一些反馈后隐式参数优先级)。

以下是名单:

  • 1)通过本地声明、导入、外部作用域、继承、无需前缀即可访问的包对象对当前调用作用域可见。
  • 2) 隐式的范围,它包含所有类型的伴生对象和包对象,它们与我们要搜索的隐式类型有某种关系(即类型的包对象,类型本身的伴生对象,它的类型构造函数(如果有),它的形参(如果有),以及它的超类型和超特征)。

如果在任何一个阶段,我们发现一个以上的隐式,静态重载规则被用来解决它。