【问题标题】:Where does Scala look for implicits?Scala 在哪里寻找隐式?
【发布时间】:2011-08-01 16:04:01
【问题描述】:

对于 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 时不抱怨歧义?

注意:这个问题的灵感来自this other question,希望以更笼统的方式陈述问题。该示例是从那里复制的,因为它在答案中被引用。

【问题讨论】:

    标签: scala implicit-conversion implicits


    【解决方案1】:

    隐式类型

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

    隐式转换

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

    "abc".map(_.toInt)
    

    String 不支持map 方法,但StringOps 支持,并且有从StringStringOps 的隐式转换可用(请参阅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] 的隐式转换可用。因此,我可以将String 传递给getIndex,它会起作用。

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

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

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

    这种语法糖被描述为 view bound,类似于 upper bound (CC &lt;: Seq[Int]) 或 lower bound (@ 987654358@)。

    上下文边界

    隐式参数中的另一个常见模式是类型类模式。这种模式可以为没有声明它们的类提供通用接口。它既可以用作桥接模式(实现关注点分离),也可以用作适配器模式。

    您提到的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 撰写的关于隐式的优秀 presentation,我衷心推荐给任何想要提高 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)(_ + _)
    }
    

    其他文件中的相同范围

    编辑:似乎这没有不同的优先级。如果您有一些演示优先级区别的示例,请发表评论。否则,不要依赖这个。

    这与第一个示例类似,但假设隐式定义位于与其用法不同的文件中。另请参阅如何使用 package objects 来引入隐含。

    一种类型的伴随对象

    这里有两个值得注意的对象伴侣。首先,查看“源”类型的对象伴侣。例如,在对象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 &gt;: 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"
    

    其他尺寸

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

    编辑

    感兴趣的相关问题:

    【讨论】:

    • 是时候开始在书中使用您的答案了,现在只需将它们放在一起即可。
    • @pedrofurla 我一直在考虑用葡萄牙语写一本书。如果有人能找到我与技术出版商的联系方式...
    • 该类型的部件的同伴的包对象也被搜索。 lampsvn.epfl.ch/trac/scala/ticket/4427
    • 在这种情况下,它是隐式范围的一部分。调用站点不必在该包中。这让我很惊讶。
    • 是的,所以stackoverflow.com/questions/8623055 具体涵盖了这一点,但我注意到您写道“以下列表旨在按优先顺序呈现......请报告。”基本上,内部列表应该是无序的,因为它们都具有相同的权重(至少在 2.10 中)。
    【解决方案2】:

    我想找出隐式参数解析的优先级,而不仅仅是它在哪里寻找,所以我写了一篇博文revisiting implicits without import tax(和implicit parameter precedence again在一些反馈后)。

    这是列表:

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

    如果在任一阶段我们发现不止一个隐式的静态重载规则用于解决它。

    【讨论】:

    • 如果您编写一些代码只是定义包、对象、特征和类,并在引用范围时使用它们的字母,这可能会得到改进。根本不需要放置任何方法声明——只需要名称和谁扩展了谁,以及在哪个范围内。
    猜你喜欢
    • 2015-11-20
    • 2015-01-16
    • 1970-01-01
    • 2019-05-02
    • 2012-05-21
    • 2015-07-28
    • 2018-06-24
    • 2017-05-23
    • 1970-01-01
    相关资源
    最近更新 更多