在隐式 class 中创建的 Scala 不兼容嵌套类型

Scala incompatible nested types created in implicit class

提供的代码片段是一个虚构的简单示例,仅用于演示问题,与实际业务逻辑类型无关。

在下面的代码中,我们在 Registry 类型中嵌套了一个 Entry 类型。

class Registry[T](name: String) {
  case class Entry(id: Long, value: T)
}

这是有道理的,因为不同注册表的条目是不同的、无法比较的类型。

然后我们可能有一个隐式操作 class,例如,用于测试,它将我们的注册表绑定到一些测试存储实现,一个简单的可变映射

object testOps {

  import scala.collection.mutable

  type TestStorage[T] = mutable.Map[Long, T]

  implicit class RegistryOps[T](val self: Registry[T])(
    implicit storage: TestStorage[T]
  ) {

    def getById(id: Long): Option[self.Entry] = 
      storage.get(id).map(self.Entry(id, _))

    def remove(entry: self.Entry): Unit       = storage - entry.id
  }
}

问题是:在 Ops 包装器内部构造的 Entry 被视为与原始注册表对象无法比较的类型

object problem {

  case class Item(name: String)

  val items = new Registry[Item]("elems")

  import testOps._

  implicit val storage: TestStorage[Item] = 
    scala.collection.mutable.Map[Long, Item](
      1L -> Item("whatever")
    )
  /** Compilation error: 
   found   : _1.self.Entry where val _1: testOps.RegistryOps[problem.Item]
   required: eta[=13=].self.Entry
  */
  items.getById(1).foreach(items.remove)
}

问题是:有没有办法声明 Ops 签名以使编译器理解我们正在使用相同的内部类型? ( 我也在 RegistryOps 中尝试过 self.type#Entry 但没有成功) 如果我错过了一些理解并且它们实际上是不同的类型,我将不胜感激任何解释和示例为什么将它们视为相同可能会破坏类型系统。谢谢!

首先,值得注意的是这里的隐式并不是真正的问题——如果你写出类似下面的内容,它会以完全相同的方式失败:

new RegistryOps(items).getById(1).foreach(e => new RegistryOps(items).remove(e))

有很多方法可以做您想做的事情,但它们并不令人愉快。一种方法是对隐式 class 进行脱糖,以便您可以让它为注册表值捕获更具体的类型:

class Registry[T](name: String) {
  case class Entry(id: Long, value: T)
}

object testOps {
  import scala.collection.mutable

  type TestStorage[T] = mutable.Map[Long, T]

  class RegistryOps[T, R <: Registry[T]](val self: R)(
    implicit storage: TestStorage[T]
  ) {
    def getById(id: Long): Option[R#Entry] = 
      storage.get(id).map(self.Entry(id, _))

    def remove(entry: R#Entry): Unit = storage - entry.id
  }

  implicit def toRegistryOps[T](s: Registry[T])(
    implicit storage: TestStorage[T]
  ): RegistryOps[T, s.type] = new RegistryOps[T, s.type](s)
}

这工作得很好,无论是在您使用它的形式,还是稍微更明确:

scala> import testOps._
import testOps._

scala> case class Item(name: String)
defined class Item

scala> val items = new Registry[Item]("elems")
items: Registry[Item] = Registry@69c1ea07

scala> implicit val storage: TestStorage[Item] = 
     |     scala.collection.mutable.Map[Long, Item](
     |       1L -> Item("whatever")
     |     )
storage: testOps.TestStorage[Item] = Map(1 -> Item(whatever))

scala> val resultFor1 = items.getById(1)
resultFor1: Option[items.Entry] = Some(Entry(1,Item(whatever)))

scala> resultFor1.foreach(items.remove)

请注意,resultFor1 的推断静态类型正是您所期望和想要的。与上面评论中提出的 Registry[T]#Entry 解决方案不同,此方法将禁止您使用相同的 T 从一个注册表中获取条目并将其从另一个注册表中删除。据推测,您专门将 Entry 设为内部案例 class 是因为您想避免这种情况。如果你不在乎,你真的应该将 Entry 提升到它自己的顶级案例 class 和它自己的 T.

附带说明一下,您可能认为只需编写以下内容即可:

implicit class RegistryOps[T, R <: Registry[T]](val self: R)(
  implicit storage: TestStorage[T]
) {
  def getById(id: Long): Option[R#Entry] = 
    storage.get(id).map(self.Entry(id, _))

  def remove(entry: R#Entry): Unit = storage - entry.id
}

但你错了,因为编译器在对隐式 class 进行脱糖处理时将生成的合成隐式转换方法将使用比你需要的更宽的 R,你将回到没有 R 时的相同情况。所以你必须自己写 toRegistryOps 并指定 s.type.

(作为脚注,我不得不说,隐式传递一些可变状态听起来绝对是一场噩梦,我强烈建议不要像您在这里做的那样远程做任何事情。 )

发帖 self-answer 希望对某人有所帮助:

我们可以将 Entry 类型从 Registry 移动到带有额外 type-parameter 的 RegistryEntry 中,然后在类型别名中绑定注册表 self-type,例如:

case class RegistryEntry[T, R <: Registry[T]](id: Long, value: T)

case class Registry[T](name: String) {
  type Entry = RegistryEntry[T, this.type]
  def Entry(id: Long, value: T): Entry = RegistryEntry(id, value)
}

这将保证 type-safety 在原始问题中请求并且 "problem" 代码片段也将编译。