Scala: método de implementación con tipo de retorno de instancia concreta
abstract-type self-type (4)
Sin embargo, ninguno de ellos realmente obliga a una implementación a devolver su propio tipo. Por ejemplo, las siguientes clases serían válidas.
Pero, ¿no es normal? De lo contrario, significaría que no podría simplemente extender A
para agregar un nuevo método por ejemplo, ya que rompería automáticamente el contrato que está tratando de crear (es decir, la copy
la nueva clase no devolvería una instancia de esta clase, pero de A
). El hecho de poder tener una clase A
perfectamente perfecta que se rompe tan pronto como la extiendes como clase B
parece mal. Pero para ser sincero, tengo problemas para poner palabras sobre los problemas que causa.
ACTUALIZACIÓN : Después de pensar un poco más sobre esto, creo que esto podría ser bueno si la verificación de tipo ("tipo de retorno == clase más derivada") se hizo solo en clases concretas y nunca en clases o rasgos abstractos. Sin embargo, no tengo conocimiento de ninguna forma de codificar eso en el sistema de tipo scala.
El hecho de que puedo hacer eso hace que, si estoy haciendo copias de objetos de los cuales la única información que tengo es que pertenecen a una subclase dada de A''s
¿Por qué no puedes simplemente devolver un Seq[Ca#Self]
? Por ejemplo, con este cambio pasando una lista de B
a createCopies
se espera como resultado una Seq[B]
(y no solo una Seq[A]
:
scala> def createCopies[CA <: A](seq: Seq[CA]): Seq[CA#Self] = seq.map(_.copy(123))
createCopies: [CA <: A](seq: Seq[CA])Seq[CA#Self]
scala> val bs = List[B]( new B(1, "one"), new B(2, "two"))
bs: List[B] = List(B@29b9ab6c, B@5ca554da)
scala> val bs2: Seq[B] = createCopies(bs)
bs2: Seq[B] = List(B@92334e4, B@6665696b)
Necesito una forma de aplicar un método en una clase abstracta para tener un tipo de retorno de la clase concreta del objeto al que se llama. El ejemplo más común es un método copy()
, y actualmente estoy usando un enfoque basado en tipos abstractos:
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)
}
Ya vi muchos enfoques, incluidos los de esta gran respuesta . Sin embargo, ninguno de ellos realmente obliga a una implementación a devolver su propio tipo. Por ejemplo, las siguientes clases serían válidas:
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, "")
}
El hecho de que puedo hacer eso hace que, si estoy haciendo copias de objetos de los cuales la única información que tengo es que pertenecen a una subclase de A
'':
// type error: Seq[A] is not a Seq[CA]!
def createCopies[CA <: A](seq: Seq[CA]): Seq[CA] = seq.map(_.copy(genNewId()))
¿Hay alguna manera mejor y segura de tipos de hacer eso?
EDITAR: Si es posible, me gustaría mantener la capacidad de crear arbitrariamente jerarquías profundas de clases abstractas. Es decir, en el ejemplo anterior, espero poder crear una clase abstracta A2
que amplíe A
, y luego proceder a crear las subclases concretas de A2
. Sin embargo, si eso simplifica el problema (como es el caso con los tipos abstractos), no necesito extender aún más las clases ya concretas.
La única solución que podía pensar era esta:
trait CanCopy[T <: CanCopy[T]] { self: T =>
type Self >: self.type <: T
def copy(newId: Int): Self
}
abstract class A(id: Int) { self:CanCopy[_] =>
def copy(newId: Int): Self
}
Lo siguiente compilaría:
class B(id: Int, x: String) extends A(id) with CanCopy[B] {
type Self = B
def copy(newId: Int) = new B(newId, x)
}
class C(id: Int, y: String, z: String) extends A(id) with CanCopy[C] {
type Self = C
def copy(newId: Int) = new C(newId, y, z)
}
Lo siguiente no compilaría:
class D(id: Int, w: String) extends A(id) with CanCopy[D] {
type Self = A
def copy(newId: Int) = new D(newId, w) // returns an A
}
class E(id: Int, v: String) extends A(id) with CanCopy[E] {
type Self = B
def copy(newId: Int) = new B(newId, "")
}
Editar
De hecho, olvidé eliminar el método de copia. Esto podría ser un poco más genérico:
trait StrictSelf[T <: StrictSelf[T]] { self: T =>
type Self >: self.type <: T
}
abstract class A(id: Int) { self:StrictSelf[_] =>
def copy(newId:Int):Self
}
No creo que sea posible en scala hacer lo que quieras.
Si tuviera que:
class Base { type A }
class Other extends Base
class Sub extends Other
Ahora ... queremos que el tipo A se refiera a "el tipo de la subclase".
Puede ver que desde el contexto de Base
, no es particularmente claro (desde la perspectiva del compilador) qué significa el "tipo de subclase" específico, sin importar qué sintaxis sería referirse a él en el padre. En Other
, significaría una instancia de Other
, pero en Sub podría significar una instancia de Sub
? ¿Estaría bien definir una implementación de su método devolviendo un Other
en Other
pero no en Sub
? Si hay dos métodos que devuelven A
, y uno se implementa en Other
y el otro en Sub
, ¿significa eso que el tipo definido en Base tiene dos significados / límites / restricciones diferentes al mismo tiempo? Entonces, ¿qué sucede si se hace referencia a A fuera de estas clases?
Lo más parecido que tenemos es this.type
. No estoy seguro si sería teóricamente posible relajar el significado de this.type
(o proporcionar una versión más relajada), pero como se implementó significa un tipo muy específico, tan específico que el único valor de retorno que satisface def foo:this.type
es this
sí mismo.
Me gustaría poder hacer lo que sugieres, pero no estoy seguro de cómo funcionaría. Imaginemos que this.type
significaba ... algo más general. ¿Qué podría ser? No podemos simplemente decir "cualquiera de los tipos definidos de this
", porque no querría que la class Subclass with MyTrait{type A=MyTrait}
sea válida. Podríamos decir "un tipo que satisface todos los tipos de this
", pero se vuelve confuso cuando alguien escribe val a = new Foo with SomeOtherMixin
... y todavía no estoy seguro de que pueda definirse de una manera que permita un implementación de Other
y Sub
definido anteriormente.
Estamos tratando de mezclar tipos estáticos y dinámicamente definidos.
En Scala, cuando dices class B { type T <: B }
, T
es específico de la instancia y B
es estático (estoy usando esa palabra en el sentido de métodos estáticos en java). Se podría decir que la class Foo(o:Object){type T = o.type}
, y T
sería diferente para cada instancia ... pero cuando se escribe type T=Foo
, Foo
es el tipo estático de la clase. También podría haber tenido una object Bar
, y se hubiera referido a algún Bar.AnotherType
. The AnotherType
, ya que es esencialmente "estático" (aunque no se llama realmente "estático" en Scala), no participa en la herencia en Foo
.
No fuerce el tipo vinculado en el lado de la declaración, a menos que necesite ese límite dentro de la definición de A
. Lo siguiente es suficiente:
abstract class A(id: Int) {
type Self
def copy(newId: Int): Self
}
Ahora fuerce el tipo de Self
en el sitio de uso:
def genNewId(): Int = ???
def createCopies[A1 <: A { type Self = A1 }](seq: Seq[A1]): Seq[A1] =
seq.map(_.copy(genNewId()))