Scala在哪里寻找隐式?


398

一个隐含的新人Scala的问题似乎是:在哪里呢编译器找implicits?我的意思是隐含的,因为这个问题似乎从来没有完全形成,就好像没有言语。:-)例如,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

注意:这个问题是由另一个问题启发而来的,希望以更笼统的方式说明问题。该示例是从那里复制的,因为答案中已提及该示例。

Answers:


554

内隐类型

可以说,Scala中的隐式是指可以“自动”传递的值,或者是指从一种类型到另一种类型的自动转换。

隐式转换

说到非常简单说一下后一种类型,如果调用一个方法m的对象上o的一类C,而该类不是不支持的方法m,然后将斯卡拉寻找从隐式转换C的东西,支持m。一个简单的例子是方法mapString

"abc".map(_.toInt)

String不支持method map,但是支持StringOps,并且存在从String到的隐式转换StringOps(请参阅implicit def augmentString参考资料Predef)。

隐式参数

另一种隐式是隐式参数。它们像其他任何参数一样传递给方法调用,但是编译器会尝试自动填充它们。如果不能,它将抱怨。一个可以明确地传递这些参数,这是怎么一个用途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]。因此,我可以将传递StringgetIndex,它将起作用。

在后台,编译器更改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)(_ + _)
}

当您仅需要将上下文边界传递给使用它们的其他方法时,上下文边界会更有用。例如,sortedon上的方法Seq需要隐式Ordering。要创建一个方法reverseSort,可以编写:

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

因为Ordering[T]已隐式传递给reverseSort,所以可以将其隐式传递给sorted

内隐来自哪里?

当编译器认为需要隐式时,要么是因为您正在调用对象类上不存在的方法,要么是因为您正在调用需要隐式参数的方法,则编译器将搜索适合需要的隐式。

该搜索遵循某些规则,这些规则定义了哪些隐式可见,哪些不可见。下表显示编译器将用于implicits搜索是从一个出色的拍摄表现有关乔希Suereth,implicits我衷心地推荐给任何人想要提高自己的知识斯卡拉。从那时起,它就得到了反馈和更新的补充。

下面数字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,因此可以Iterable在上调用方法Option,或传递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"

这是斯卡拉如何发现隐含的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,也没有可查看的“源”类型。显然,这是里面找到它A,这是一个类型参数Ordering

这也是各种期望的收集方法的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"

其他尺寸

我敢肯定这是个玩笑,但是这个答案可能不是最新的。因此,请勿将此问题视为正在发生的事情的最终仲裁者,如果您确实注意到它已经过时,请通知我以便我可以解决。

编辑

感兴趣的相关问题:


60
现在该是您开始在书中使用答案的时候了,现在只需将所有内容放在一起即可。
pedrofurla 2011年

3
@pedrofurla我曾被考虑用葡萄牙语写书。如果有人可以找到我与技术发行商的联系...
Daniel C. Sobral

2
还搜索该类型的各个部分的伴随对象的包装对象。lampsvn.epfl.ch/trac/scala/ticket/4427
返璞词

1
在这种情况下,它是隐式作用域的一部分。呼叫站点不必在该软件包中。这令我惊讶。
retronym

2
是的,所以stackoverflow.com/questions/8623055专门涵盖了这一点,但是我注意到您写道:“以下列表旨在按优先顺序显示...请报告。” 基本上,内部列表应该是无序的,因为它们的权重都相同(至少在2.10中)。
尤金·横田

23

我想找出隐式参数解析的优先级,而不仅仅是查找的位置,所以我写了一篇博客文章,重新讨论了无须缴纳进口税的隐式参数(并在反馈后再次隐式参数优先级)。

这是清单:

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

如果在任一阶段我们发现一个以上的隐式静态重载规则都可以用来解决该问题。


3
如果您编写了一些代码来定义包,对象,特征和类,并在引用范围时使用它们的字母,则可以改进此功能。根本不需要放置任何方法声明-只是名称和谁扩展了谁,以及在哪个范围内。
Daniel C. Sobral
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.