What is the correct way to specify type variance for methods in a companion object
For me one of the more confusing aspects of the Scala type system is understanding covariance, contravariance, type bounds etc.
I am trying to create a generic Repository
trait that can be extended by companion objects objects of classes that extend a Page
trait. The idea is that the companion object will be responsible for creating new instances etc. These page instances will need to be cleaned up if they haven't been accessed within some period of time. Thus the base Repository
trait will register them in a list of repositories that can be checked in a background actor thread.
Below is a stripped down version of the code. I'm getting a type mismatch
error on the call to register(pages)
. The compiler found HashMap[String, T]
but is expecting HashMap[String, Page]
. I can't figure out what to do to make the compiler happy. I can define the register method as def register[T <: Page](repo: HashMap[String, T) ...
but that just defers the problem to the reference of the var repos
which I cannot qualify generically. I would appreciate it if someone could demonstrate the correct way to specify the types.
EDIT I can get it to work if I declare the hashmap as HashMap[String, Page]
and then cast the page
value retrieved from the hashmap with page.asInstanceOf[String, T]
. Is there a way to avoid the cast?
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: Ha开发者_运维问答shMap[String, Page]) {
repos = repo :: repos
}
...
}
class CoolPage extends Page
object CoolPage extends Page.Repository[CoolPage] {
def newPage = new CoolPage
}
val p = CoolPage()
First thing to note is that mutable HashMap
is invariant: class HashMap [A, B]
. Though the immutable version is covariant on values: class HashMap [A, +B]
.
Second thing to note is that your repos
variable is meant to be a polymorphic collection which means that some compile time type information is lost when you put stuff there.
But since you use the mutable HashMap
, the repos
can't actually be a correct polymorphic collection because of HashMap
invariance. To illustrate why let's suppose Page is a class (so we can instantitate it) and we put a HashMap[String, CoolPage] in the repos
list. Then we could do this:
val m = repos.head // HashMap[String, Page]
m.put("12345678", new Page) // We just added a Page to HashMap[String, CoolPage]
So the compiler gives you an error to protect you from this.
I guess you can fix your code by making Repository covariant:
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()
}
}
}
And changing repos
to be a list of Repository[Page]
:
private var repos: List[Repository[Page]] = Nil
private def register(repo: Repository[Page]) {
repos = repo :: repos
}
And remember that polymorphic collections (like repos
) make you lose compile time type information of elements: if you put a Repository[CoolPage]
there you only get Repository[Page]
back and have to deal with it.
update: removed .asInstance[T]
from the Repository
code by making pages
private[this]
.
精彩评论