Scala 在哪里寻找隐式?

问题描述

隐式类型

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

隐式转换

简单说一下后一种类型,如果调用m的对象的方法,而该类不支持方法那么 Scala 会寻找从隐式转换到支持方法一个简单的例子是on的方法o``C``m``C __m``map``String

"abc".map(_.toInt)

String不支持方法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]. 因此,我可以将 a 传递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)(_ + _)
}

它还有一个语法糖,称为 context bound ,由于需要引用隐式,它变得不那么有用。该方法的直接转换如下所示:

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)(_ + _)
}

当您只需要将上下文边界 传递 给使用它们的其他方法时,上下文边界会更有用。例如,方法sortedonSeq需要一个隐式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. 现在查看关联类型
    • 类型的伴随对象
    • 参数类型的隐式范围
    • 类型参数的隐式范围
    • 嵌套类型的外部对象
    • 其他尺寸

让我们为他们举一些例子:

当前范围中定义的隐式

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期望 aTraversableOnce,但事实Option并非如此。然后编译器在Option的对象伴侣内部查找并找到到 的转换Iterable,即 a TraversableOnce,从而使该表达式正确。

二、预期类型的​​伴生对象:

List(1, 2, 3).sorted

方法sorted采用隐式Ordering. 在这种情况下,它会在 object 内部Ordering,与 class 相伴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.

参数类型的隐式范围

如果你有一个带有参数 type 的方法A,那么 type 的隐式范围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)

类型参数的隐式范围

这是使类型类模式真正起作用所必需的。例如,考虑一下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], where B >: A)。里面没有任何这样的东西Ordering,也没有可供查看的“源”类型。显然,它在里面找到它A,这是一个 类型Ordering参数。

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

Ordering定义为trait Ordering[T],其中T是类型参数。之前我说过Scala看里面的类型参数,没有多大意义。上面隐式查找的是Ordering[A],其中A是实际类型,而不是类型参数:它是 的类型 参数Ordering。请参阅 Scala 规范的第 7.2 节。

嵌套类型的外部对象

我实际上还没有看到这样的例子。如果有人可以分享一个,我将不胜感激。原理很简单:

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"

其他尺寸

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

解决方法

对于 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呢?

注意: 这个问题的灵感来自另一个问题,希望以更一般的方式说明问题。该示例是从那里复制的,因为它在答案中被引用。