I need a way to enforce a method in an abstract class to have a return type of the concrete class of the object it is called on. The most common example is a copy()
method, and I'm currently using an approach based on abstract types:
abstract class A(id: Int) {
type Self <: A
def copy(newId: Int): Self
}
class B(id: Int, x: String) extends A(id) {
type Self = B
def copy(newId: Int) = new B(newId, x)
}
class C(id: Int, y: String, z: String) extends A(id) {
type Self = C
def copy(newId: Int) = new C(newId, y, z)
}
I already saw many approaches, including the ones in this great answer. However, none of them really forces a implementation to return its own type. For example, the following classes would be valid:
class D(id: Int, w: String) extends A(id) {
type Self = A
def copy(newId: Int) = new D(newId, w) // returns an A
}
class E(id: Int, v: String) extends A(id) {
type Self = B
def copy(newId: Int) = new B(newId, "")
}
The fact that I can do that causes that, if I am doing copies of objects of which the only information I have is that they are of a given subclass of A
's:
// type error: Seq[A] is not a Seq[CA]!
def createCopies[CA <: A](seq: Seq[CA]): Seq[CA] = seq.map(_.copy(genNewId()))
Is there a better, type-safe way I can do that?
EDIT: If possible, I would like to keep the ability to create arbitrarily deep hierarchies of abstract classes. That is, in the previous example, I'm expecting to be able to create an abstract class A2
that extends A
, and then proceed to create A2
's concrete subclasses. However, if that simplifies the problem (as it's the case with abstract types), I do not need to further extend already concrete classes.
The only solution I could think of was this one:
The following would compile:
The following would not compile:
Edit
I actually forgot to remove the copy method. This might be a bit more generic:
Do not force the type bound on the declaration side, unless you need that bound within the definition of
A
itelf. The following is sufficient:Now force the
Self
type on the use site:But isn't it normal? Otherwise it would mean that you could not merely extend
A
to add a new method by example, as it would automatically break the contract that you are trying to create (that is, the new class'scopy
would not return an instance of this class, but ofA
). The very fact of being able to have a perfectly fine classA
that breaks as soon as you extend it as classB
feels wrong to me. But to be honest I have trouble putting words on the problems it causes.UPDATE: After thinking a bit more about this, I think this could be sound if the type check ("return type == most-derived class") was made only in concrete classes and never on abstract classes or traits. I am not aware of any way to encode that in the scala type system though.
Why can't you just return a
Seq[Ca#Self]
? By example, with this change passing a list ofB
tocreateCopies
will as expected return aSeq[B]
(and not just aSeq[A]
:I don't think it's possible in scala to do what you want.
If I were to:
Now... we want type A to refer to "the type of the subclass."
You can see that from the context of
Base
, it's not particularly clear (from the compiler's perspective) what the specific "type of the subclass" means, nevermind what the syntax would be to refer to it in the parent. InOther
it would mean an instance ofOther
, but in Sub it might mean an instance ofSub
? Would it be OK to define an implementation of your method returning anOther
inOther
but not inSub
? If there are two methods returningA
's, and one is implemented inOther
and the other inSub
, does that mean the type defined in Base has two different meanings/bounds/restrictions at the same time? Then what happens ifA
is referred to outside of these classes?The closest thing we have is
this.type
. I'm not sure if it would be theoretically possible to relax the meaning ofthis.type
(or provide a more relaxed version), but as implemented it means a very specific type, so specific that the only return value satisfyingdef foo:this.type
isthis
itself.I'd like to be able to do what you suggest, but I'm not sure how it would work. Let's imagine that
this.type
meant... something more general. What would it be? We can't just say "any of the defined types ofthis
," because you wouldn't wantclass Subclass with MyTrait{type A=MyTrait}
to be valid. We could say "a type satisfying all of the types ofthis
," but it gets confusing when someone writesval a = new Foo with SomeOtherMixin
... and I'm still not sure it could be defined in a way that would enable an implementation of bothOther
andSub
defined above.We're sort-of trying to mix static and dynamically defined types.
In Scala, when you say
class B { type T <: B }
,T
is specific to the instance, andB
is static (I'm using that word in the sense of static methods in java). You could sayclass Foo(o:Object){type T = o.type}
, andT
would be different for every instance.... but when you writetype T=Foo
,Foo
is the statically specified type of the class. You could just as well have had anobject Bar
, and had referred to someBar.AnotherType
. TheAnotherType
, since it's essentially "static," (though not really called "static" in Scala), doesn't participate in inheritance inFoo
.