Scala中的<:<运算符是如何工作的?

7
在Scala中,有一个类<:<用于证明类型约束。该类来源于Predef.scala:
  sealed abstract class <:<[-From, +To] extends (From => To) with Serializable
  private[this] final val singleton_<:< = new <:<[Any,Any] { def apply(x: Any): Any = x }
  implicit def $conforms[A]: A <:< A = singleton_<:<.asInstanceOf[A <:< A]

它的使用示例可以在TraversableOncetoMap方法中找到:

def toMap[T, U](implicit ev: A <:< (T, U)): immutable.Map[T, U] =

我不理解的是这个是如何工作的。我知道A <:< B在语法上等同于类型<:<[A, B]。但是我不明白编译器如何能够找到该类型的隐式值,仅当A <: B时才能找到。我假设$conforms定义中的asInstanceOf调用以某种方式使这成为可能,但是如何实现的呢?此外,使用抽象类的单例实例是否重要,而不是只使用object

我知道关于这个类还有一些其他问题,问它是用来做什么的,或者如何使用它,但没有问它是如何工作的:https://dev59.com/-HE85IYBdhLWcg3wzm1p https://dev59.com/a3A75IYBdhLWcg3wMl8Z而且有一个完全重复的问题,但它本身被错误地关闭为重复问题,我认为它所谓的重复问题并没有问到实现的工作原理是什么: https://stackoverflow.com/questions/11464976/ - Rag
1
还有https://dev59.com/UXvaa4cB1Zd3GeqPF6Zy#21306762和https://dev59.com/E2Eh5IYBdhLWcg3wMA0t#22717040。 - Régis Jean-Gilles
@RégisJean-Gilles 哎呀!谢谢。 - Rag
1个回答

13

假设我们有以下简单类型层次结构:

trait Foo
trait Bar extends Foo

我们可以要求证明Bar扩展了Foo

val ev = implicitly[Bar <:< Foo]

如果我们在控制台中使用 -Xprint:typer 运行此程序,我们将看到以下内容:

private[this] val ev: <:<[Bar,Foo] =
  scala.this.Predef.implicitly[<:<[Bar,Foo]](scala.this.Predef.$conforms[Bar]);
所以编译器选择了$conforms[Bar]作为我们要求的隐式值。当然,这个值的类型是Bar <:< Bar,但由于<:<在其第二个类型参数中是协变的,因此这是Bar <:< Foo的子类型,因此它符合要求。
(这里有一些魔法,即Scala编译器知道如何找到其正在寻找的类型的子类型,但这是一个相当通用的机制,在行为上并不令人惊讶。)
现在假设我们要求证Bar扩展自String
val ev = implicitly[Bar <:< String]

如果你打开了-Xlog-implicits,你会看到这个:

<console>:9: $conforms is not a valid implicit value for <:<[Bar,String] because:
hasMatchingSymbol reported error: type mismatch;
 found   : <:<[Bar,Bar]
 required: <:<[Bar,String]
       val ev = implicitly[Bar <:< String]
                          ^
<console>:9: error: Cannot prove that Bar <:< String.
       val ev = implicitly[Bar <:< String]
                          ^
编译器再次尝试Bar <:< Bar,但由于Bar不是String,所以它不是Bar <:< String的子类型,因此这不是我们需要的。但是,$conforms是编译器可以获取<:<实例的唯一位置(除非我们自己定义了,这将是危险的),因此它非常正确地拒绝编译这个无聊的东西。
为了回答您的第二个问题:必须使用<:<[-From, +To]类,因为我们需要类型参数来使这个类型类有用。单例Any <:< Any值同样可以被定义为对象 - 使用val和匿名类的决定可能更简单,但这是一个您永远不需要担心的实现细节。

非常好的答案,谢谢!您能否添加一些关于为什么扩展 (From => To) 并实现 apply 的信息?我假设该方法从未被调用。 - Rag
1
它在许多情况下都会被使用。以toMap定义为例,例如这一行,如果没有一个从A(T, U)的隐式转换在作用域内,它将无法编译。 - Travis Brown

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接