I have an Accessor class that retrieves Items. It can also take an Item as a parameter and return the latest version of that Item from the database. When it creates an Item it passes itself as a parameter to the Item.
I want the compiler to statically require that an Accessor instance will only accept Items that were created by itself. This was covered by How to use Scala's singleton-object types? however I also want an Item instance to be able to pass itself as a parameter to its own Accessor to retrieve the latest version of itself.
The difficulty with this is that a type parameter in the Item class definition like so
class Item[A <: Accessor](acc: A)
cannot refer to the type of acc itself. From the perspective of Item acc.type <: A <: Accessor so this in Item, which is an Item[A], is not an Item[acc.type]. Thus this doesn’t work:
class Item[A <: Accessor](acc: A) {
acc.accept(this) // Type Mismatch: found Item[A], required Item[Item.this.acc.type]
}
class Accessor {
def make() = new Item[this.type](this)
def accept(item: Item[this.type]) = "accepted"
}
I then tried this:
object A1 extends Accessor[A1.type](A1) // illegal cyclic reference involving object A1
class Item[+A <: Accessor[A]](acc: A) {
acc.accept(this)
A1.accept(this) // Compile error (good)
}
class Accessor[+A <: Accessor[A]](me: => A) {
def make = new Item[A](me)
def accept(item: Item[A]) = "accepted"
}
where the problem is actually creating an instance of Accessor.
I tried a variation on the above which turned out to be a messier incarnation of the same fundamental dilemma:
object A1 extends Accessor {
type A = A1.type
def me = A1
}
class Item[+AA <: Accessor](acc: AA {type A = AA}) {
acc.accept(this)
A1.accept(this) // Compile error (good)
}
class Accessor {
type A <: Accessor
def me: A // can't do {type A = A} because its a cyclic error again
def make = new Item[A](me) // Type Mismatch: found this.A, required this.A {type A = Accessor.this.A}
def accept(item: Item[A]) = "accepted"
}
Finally I tried making the A type parameter contravariant so that Item[A] is a subtype of Item[acc.type] and will be accepted by acc.
val a1 = new Accessor
val a2 = new Accessor
val item1 = a1.make
val item2 = a2.make
val itemA = new Item[Accessor](a2)
val item12 = new Item[A1.type](a2) // compile error (good)
a1.accept(itemA) // no compile error (bad), but I can prevent creation of Item[Accessor]s
a1.accept(item2) // compile error (good)
class Item[-A <: Accessor](acc: A) {
acc.accept(this)
val acc2 = new Accessor
acc2.accept(this) // compile error (good)
// here Item[Accessor] <: Item[A] <: Item[acc.type]
// and Item[Accessor] <: Item[acc2.type]
// but Item[A] is not necessarily <: Item[acc2.type]
}
class Accessor {
def make() = new Item[this.type](this)
def accept(item: Item[this.type]) = "accepted"
}
This comes closest to working of anything I have tried. The only problem is it stuffs up my object hierarchy because I can’t do this:
class ImmutableAccessor extends Accessor
class ImmutableItem[-A <: ImmutableAccessor](acc: A) extends Item[A] // fails due to contravariance in A
If only there were some way to specify that a type parameter must be a singleton type. So for example you could say (I’m inventing notation here)
class Item[A:type <: Accessor](acc: A)
And A would then be the singleton type of acc and we’d be laughing.
The secret sauce we need here is to exploit the <:< class found in Predef, using an implicit value. You are quite right that the “with Singleton” didn’t do everything we wanted.
Now the inheritance works better. We can do stuff like the following
And we get compile time errors exactly where we want.