Scala несовместимые вложенные типы, созданные в неявном классе - PullRequest
0 голосов
/ 04 февраля 2019

Предоставленный фрагмент кода представляет собой вымышленный минималистический пример, демонстрирующий проблему, не связанную с реальными типами бизнес-логики.

В приведенном ниже коде у нас есть вложенный тип Entry внутри Registrytype.

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

Проблемаis: Entry, созданный внутри оболочки Ops, обрабатывается как несопоставимый тип с исходным объектом Registry

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

Вопрос: Есть ли способ объявить Opsподписи, чтобы компилятор понимал, что мы работаем с одним и тем же внутренним типом?(Я также безуспешно пытался self.type#Entry в RegistryOps). Если мне не хватает некоторого понимания, и они на самом деле являются разными типами, я был бы признателен за любые объяснения и примеры, почему рассмотрение их как одного и того же может нарушить систему типов.Спасибо!

Ответы [ 2 ]

0 голосов
/ 21 июня 2019

Публикация самоответа в надежде, что он кому-нибудь поможет:

Мы можем переместить 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)
}

Это будет гарантировать безопасность типов, запрошенную в исходном вопросе, и фрагмент кода «проблема» также скомпилируется.

0 голосов
/ 04 февраля 2019

Для начала стоит отметить, что неявность здесь не является проблемой - если вы напишете что-то вроде следующего, она потерпит неудачу точно таким же образом:

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.

(В качестве сноски я должен сказать, что наличие какого-то изменчивого состояния, которое вы передаете неявно, звучит как абсолютный кошмар, иЯ настоятельно рекомендую не делать ничего удаленно, как то, что вы делаете здесь.)

...