使用Scala的隐式转换实现类型相等性

10

我一直在阅读关于Scala类型级编程的内容,主要是Apocalisp博客和Alexander Lehmann的YouTube演讲。

我有一个问题,可能非常基础,就是使用implicitly来比较两个类型,如下所示:

implicitly[Int =:= Int]

Apocalisp博客的Mark说:

这对于捕获在作用域中且类型为T的隐式值非常有用。

我知道如何使它工作,但我不确切知道它为什么能够工作,所以我不想继续前进。

在上面的例子中,是否存在一个类型为“Int”的隐式值在作用域中,而'implicitly'从空气中抓取,使得代码能够编译?这如何与'function1'返回类型匹配?

res0: =:=[Int,Int] = <function1>

此外,这个implicit是从哪里来的呢?对于我的特质`Foo`的情况呢?为什么会这样?
implicitly[Foo =:= Foo] 

编译?在这种情况下,“Foo”隐式会从哪里来呢?
如果这是一个非常愚蠢的问题,请提前道歉,感谢任何帮助!
1个回答

18

X =:= Y 只是类型 =:=[X, Y] 的语法糖(中缀表示法)。

因此,当你执行 implicitly[Y =:= Y] 时,你只是在查找类型为 =:=[X, Y] 的隐式值。 =:= 是在 Predef 中定义的通用 trait。

另外,=:= 是一个有效的类型名称,因为类型名称(就像任何标识符一样)可以包含特殊字符。

从现在开始,让我们将 =:= 重命名为 IsSameType 并删除中缀表示法,以使我们的代码看起来更直观、不那么神奇。 这给我们带来了 implicitly[IsSameType[X,Y]]

以下是该类型定义的简化版本:

sealed abstract class IsSameType[X, Y]
object IsSameType {
   implicit def tpEquals[A] = new IsSameType[A, A]{}
}

注意,tpEquals 为任何类型 A 提供了一个隐式值 IsSameType[A, A]。 换句话说,它仅在 XY 是同一类型时提供 IsSameType[X, Y] 的隐式值。 因此,implicitly[IsSameType[Foo, Foo]] 可以编译通过。 但是,implicitly[IsSameType[Int, String]] 不行,因为在这里没有适用于类型 IsSameType[Int, String] 的隐式值,因为 tpEquals 在这里不适用。
所以,通过这个非常简单的构造,我们能够静态检查某些类型 X 是否与另一个类型 Y 相同。
现在这里有一个示例,展示了它可能会如何有用。假设我想定义一个 Pair 类型(忽略它已经存在于标准库中的事实):
case class Pair[X,Y]( x: X, y: Y ) {
  def swap: Pair[Y,X] = Pair( y, x )
}
Pair 是由其两个元素的类型参数化的,这些元素可以是任何类型,最重要的是它们没有关联。 那么如果我想定义一个方法 toList 来将这个 pair 转换成一个包含 2 个元素的列表呢? 只有在 XY 相同时,这个方法才真正有意义,否则我将被迫返回一个 List[Any]。 而我肯定不希望改变 Pair 的定义为 Pair[T]( x: T, y: T ),因为我真的想要能够拥有异构类型的 pair。 毕竟,只有在调用 toList 时,我才需要 X == Y。所有其他方法(例如 swap)都应该可用于任何类型的异构 pair。 因此,最终我真正想要的是在调用 toList 时静态确保 X == Y,这样就可以一致地返回一个 List[X](或者一个等同的 List[Y])。
case class Pair[X,Y]( x: X, y: Y ) {
  def swap: Pair[Y,X] = Pair( y, x )
  def toList( implicit evidence: IsSameType[X, Y] ): List[Y] = ???
}

但是在实际实现toList时仍然存在一个严重的问题。如果我尝试编写显而易见的实现,它将无法编译:

def toList( implicit evidence: IsSameType[X, Y] ): List[Y] = List[Y]( x, y )

编译器会报告x不是类型Y。实际上,对于编译器来说,XY仍然是不同的类型。 只有通过精心构造,我们才能静态地确保X == Y(即,toList需要一个隐式值为IsSameType[X, Y]的事实,并且它们只能由tpEquals方法提供,前提是X == Y)。 但编译器肯定不会解密这个巧妙的构造,以得出X == Y的结论。
为了解决这种情况,我们可以提供一个从X到Y的隐式转换,前提是我们知道X == Y(或者换句话说,在范围内有一个IsSameType[X, Y]的实例)。
// A simple cast will do, given that we statically know that X == Y
implicit def sameTypeConvert[X,Y]( x: X )( implicit evidence: IsSameType[X, Y] ): Y = x.asInstanceOf[Y]

现在,我们实现的toList终于编译正常了:通过隐式转换sameTypeConvertx将简单地转换为Y
最后的微调是,我们可以进一步简化:鉴于我们已经将一个隐式值(evidence)作为参数,为什么不让这个值实现转换呢?如下所示:
sealed abstract class IsSameType[X, Y] extends (X => Y) {
  def apply( x: X ): Y = x.asInstanceOf[Y]
}
object IsSameType {
   implicit def tpEquals[A] = new IsSameType[A, A]{}
}    

我们现在可以移除方法sameTypeConvert,因为隐式转换现在由IsSameType实例自身提供。现在IsSameType具有双重作用:静态确保X == Y,并(如果是)提供隐式转换,实际上允许我们将X的实例视为Y的实例。
我们现在基本上重新实现了在Predef中定义的类型=:=
更新:从评论中看来,使用asInstanceOf似乎让人们感到困扰(即使它只是一个实现细节,IsSameType的任何用户都不需要进行转换)。事实证明,即使在实现中也很容易摆脱它。看这里:
sealed abstract class IsSameType[X, Y] extends (X => Y) {
  def apply(x: X): Y
}
object IsSameType {
  implicit def tpEquals[A] = new IsSameType[A, A]{
    def apply(x: A): A = x
  }
}

基本上,我们只是保留了apply的抽象,只在tpEquals中实现它,在那里我们(和编译器)知道传递的参数和返回值确实具有相同的类型。因此不需要任何转换。就是这样。
请注意,最终生成的字节码仍然存在相同的转换,但现在从源代码中消失了,并且从编译器的角度来看是可证明正确的。虽然我们引入了一个额外的(匿名)类(因此从抽象类到具体类还有一个附加的间接),但由于我们处于“单态方法分派”的简单情况下(如果您对虚拟机的内部工作原理感兴趣,请查找),它应该在任何良好的虚拟机上运行得像闪电一样快。尽管这可能使虚拟机更难将调用内联到apply(运行时虚拟机优化是一种黑色的艺术,很难做出明确的陈述)。
最后要强调的是,如果可以证明是正确的,则在代码中有转换并不是什么大问题。毕竟,标准库自己最近也有着完全相同的转换(现在整个实现已经重新设计,看起来更加强大,但仍然在其他地方包含转换)。如果对于标准库来说足够好,那对我也足够好。

apply 方法如何作为隐式转换?在 repl 中,如果我执行 implicit def blah = new SomeClass,其中 SomeClass 有一个 apply 方法来进行所需的转换,它不起作用 - 没有可用的隐式转换。 - Rag
非常好的问题,你实际上刚刚发现了我的代码中的一个错误。在Scala中,仅仅定义apply就足以将一个对象用作函数(多亏了简单的语法糖),但是该对象仍然不是一个真正的函数,因此即使它有一个apply方法,也不能作为隐式转换的候选。所以我在代码中的修复方法是让IsSameType扩展(X => Y)(如果你查看Predef=:=的定义,你会发现它确实扩展了From => To)。感谢你指出这个问题,我已经更新了我的答案。 - Régis Jean-Gilles
2
我认为这里有一些误解。我的回答采取的方法是假装标准库中不存在=:=,并逐步从头开始实现它。在此过程中,我揭示了=:=(在我的情况下命名为IsSameType)的内部工作方式,这确实需要使用asInstanceOf(请参见原始代码:https://github.com/scala/scala/blob/v2.11.8/src/library/scala/Predef.scala#L406)。 - Régis Jean-Gilles
如果您为IsSameType[X,Y]定义了一对方法:def to(x : X) : Ydef from(y: Y) : X,则不需要使用asInstanceOf。https://gist.github.com/edgarklerks/b57dbf554bcee21ae389f2aee45b5bba - Edgar Klerks
1
@Edgar Klerks:真的没有必要使用一对方法来摆脱类型转换,而且无论如何,转换都应该是隐式的。但你完全正确,强制类型转换并不是必须的,只是最自然的实现方式,因为我们确切地知道这两种类型实际上是相同的,所以我们只需要说服编译器离开我们的领地。如果想要在不使用强制类型转换的情况下进行直接实现,可以参见我的更新。简短的答案是,只有当我们想要在IsSameType中正确实现“apply”方法时才需要进行类型转换。 - Régis Jean-Gilles
显示剩余2条评论

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