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

时间:2019-02-04 09:30:46

标签: scala inner-classes implicit scala-2.12

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

在下面的代码中,我们在new File("my.csv") << vars.get("foo") << System.getProperty("line.separator") 类型中嵌套了Entry类型。

Registry

这很有意义,因为不同注册表的条目是不同的,不可比拟的类型。

然后,我们可能有一个隐式的Ops类,例如在测试中使用的类,它将我们的注册表绑定到某些测试存储实现(一个简单的可变映射)

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

问题是::在Ops包装器中构造的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 } } 被视为原始注册表对象无法比拟的类型

Entry

问题是:有没有一种方法可以声明Ops签名,以使编译器了解我们正在使用相同的内部类型? ( 我也尝试过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$0$1.self.Entry */ items.getById(1).foreach(items.remove) } 中的self.type#Entry,但没有运气) 如果我错过了一些理解并且它们实际上是不同的类型,那么我将不胜感激任何解释和示例,为什么将它们视为相同可能会破坏类型系统。谢谢!

2 个答案:

答案 0 :(得分:4)

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

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

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

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成为了一个内部案例类,因为您想避免这种事情。如果您不在乎,您真的应该将Entry提升为具有自己的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
}

但是您会错的,因为编译器将在对除错类进行除错时将产生的综合隐式转换方法将使用比您需要的R更宽的R,并且您将回到相同的情况没有toRegistryOps。因此,您必须编写自己的s.type并指定struct inventory_item { int bananas; int apples; int pineapples; }; inventory_item first_item = { bananas: 2, apples: 49, pineapples: 4 };

(作为一个脚注,我不得不说,具有某种可变状态,即您隐式地绕行听起来像是一场绝对的噩梦,我强烈建议您不要像在这里所做的那样远程进行任何操作。)< / p>

答案 1 :(得分:0)

发布自我解答希望它可以帮助某人:

我们可以使用额外的类型参数将Entry类型从Registry移到RegistryEntry中,并在其中将注册表自类型绑定为类型别名,例如:

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)
}

这将保证原始问题中要求的类型安全性,并且“问题”代码段也将编译。