Как правильно указать вариантность типов для методов в сопутствующем объекте

Для меня одним из наиболее запутанных аспектов системы типов Scala является понимание ковариантности, контравариантности, границ типов и т. д.

Я пытаюсь создать общий признак Repository, который может быть расширен объектами-компаньонами объектов классов, которые расширяют признак Page. Идея состоит в том, что объект-компаньон будет отвечать за создание новых экземпляров и т. д. Эти экземпляры страниц необходимо будет очистить, если к ним не обращались в течение некоторого периода времени. Таким образом, базовый трейт Repository зарегистрирует их в списке репозиториев, которые можно проверить в потоке фонового актора.

Ниже приведена урезанная версия кода. Я получаю сообщение об ошибке type mismatch при вызове register(pages). Компилятор нашел HashMap[String, T], но ожидает HashMap[String, Page]. Я не могу понять, что сделать, чтобы компилятор был доволен. Я могу определить метод регистрации как def register[T <: Page](repo: HashMap[String, T) ..., но это просто откладывает проблему до ссылки на var repos, которую я не могу квалифицировать в общем. Я был бы признателен, если бы кто-нибудь мог продемонстрировать правильный способ указания типов.

EDIT Я могу заставить его работать, если объявлю хэш-карту как HashMap[String, Page], а затем приведу значение page, полученное из хэш-карты, с помощью page.asInstanceOf[String, T]. Есть ли способ избежать броска?

trait Page {
  val id = Random.hex(8)
  private var lastAccessed = new Date
  ...
}

object Page {
  import scala.collection.mutable.HashMap

  trait Repository[T <: Page] {
    private val pages = new HashMap[String, T]
    register(pages)

    def newPage: T

    def apply(): T = {
      val page = newPage
      pages(page.id) = page
      page
    }

    def apply(id: String): T = {
      pages.get(id) match {
        case Some(page) =>
          page.lastAccessed = now
          page
        case None =>
          this()
      }
    }
    ...
  }

  private var repos: List[HashMap[String, Page]] = Nil

  private def register(repo: HashMap[String, Page]) {
    repos = repo :: repos
  }
  ...
}

class CoolPage extends Page

object CoolPage extends Page.Repository[CoolPage] {
  def newPage = new CoolPage
}

val p = CoolPage()

person sellmerfud    schedule 11.03.2011    source источник


Ответы (1)


Первое, что нужно отметить, это то, что изменяемый HashMap является инвариантным: class HashMap [A, B]. Хотя неизменная версия ковариантна по значениям: class HashMap [A, +B].

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

Но поскольку вы используете изменяемый HashMap, repos на самом деле не может быть правильной полиморфной коллекцией из-за HashMap инвариантности. Чтобы проиллюстрировать, почему давайте предположим, что Page является классом (чтобы мы могли создать его экземпляр), и поместим HashMap[String, CoolPage] в список repos. Тогда мы могли бы сделать это:

val m = repos.head // HashMap[String, Page]
m.put("12345678", new Page) // We just added a Page to HashMap[String, CoolPage]

Таким образом, компилятор выдает вам ошибку, чтобы защитить вас от этого.

Я думаю, вы можете исправить свой код, сделав репозиторий ковариантным:

trait Repository[+T <: Page] {
  private[this] val pages = new HashMap[String, T]
  register(this)

  def newPage: T

  def apply(): T = {
    val page = newPage
    pages(page.id) = page
    page
  }

  def apply(id: String): T = {
    pages.get(id) match {
      case Some(page) =>
        page.lastAccessed = new Date
        page
      case None =>
        this()
    }
  }
}

И изменение repos на список Repository[Page]:

private var repos: List[Repository[Page]] = Nil

private def register(repo: Repository[Page]) {
  repos = repo :: repos
}

И помните, что полиморфные коллекции (такие как repos) заставляют вас терять информацию о типе элементов во время компиляции: если вы поместите туда Repository[CoolPage], вы получите только Repository[Page] обратно, и вам придется иметь дело с этим.

обновление: удалено .asInstance[T] из кода Repository, сделав pages private[this].

person Oleg Galako    schedule 11.03.2011
comment
Хороший ответ. Я пропустил немного о том, что изменчивый HashMap является инвариантным. Дох! Ваше описание в точку. Мне также нравится трюк с private[this], чтобы компилятор знал, что pages не будет использоваться нигде, что может вызвать проблемы с дисперсией. Спасибо за помощь. - person sellmerfud; 11.03.2011