Scala: метод реализации с возвращаемым типом конкретного экземпляра

Мне нужен способ принудительного применения метода в абстрактном классе, чтобы иметь возвращаемый тип конкретного класса объекта, на который он вызван. Наиболее распространенным примером является метод copy(), и в настоящее время я использую подход, основанный на абстрактных типах:

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)
}

Я уже видел много подходов, в том числе в этот отличный ответ. Однако ни одна из них не заставляет реализацию возвращать свой собственный тип. Например, допустимы следующие классы:

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, "")
}

Тот факт, что я могу это сделать, приводит к тому, что если я делаю копии объектов, единственной информацией которых я являюсь, является то, что они относятся к данному подклассу A:

// type error: Seq[A] is not a Seq[CA]!
def createCopies[CA <: A](seq: Seq[CA]): Seq[CA] = seq.map(_.copy(genNewId()))

Есть ли лучший способ, безопасный по типу, который я могу сделать?

EDIT: если возможно, я хотел бы сохранить возможность создавать произвольно глубокие иерархии абстрактных классов. То есть, в предыдущем примере я ожидаю, что смогу создать абстрактный класс A2, который расширяет A, а затем приступит к созданию конкретных подклассов A2. Однако, если это упрощает проблему (как в случае с абстрактными типами), мне больше не нужно расширять уже конкретные классы.

Ответ 1

Единственное решение, о котором я подумал, было следующим:

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
}

Скомпилировалось следующее:

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)
}

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

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, "")
}

Изменить

Я действительно забыл удалить метод копирования. Это может быть несколько более общим:

trait StrictSelf[T <: StrictSelf[T]] { self: T =>
  type Self >: self.type <: T
}

abstract class A(id: Int) { self:StrictSelf[_] =>
  def copy(newId:Int):Self
}

Ответ 2

Не заставляйте привязку типа на стороне объявления, если вам не нужна эта граница в определении A itelf. Достаточно:

abstract class A(id: Int) {
  type Self
  def copy(newId: Int): Self
}

Настройте тип Self на используемом сайте:

def genNewId(): Int = ???
def createCopies[A1 <: A { type Self = A1 }](seq: Seq[A1]): Seq[A1] = 
  seq.map(_.copy(genNewId()))

Ответ 3

Я не думаю, что возможно в scala делать то, что вы хотите.

Если бы я:

class Base { type A }
class Other extends Base
class Sub extends Other

Теперь... мы хотим, чтобы тип A ссылался на "тип подкласса".

Вы можете видеть это из контекста Base, это не особенно ясно (с точки зрения компилятора), что означает конкретный "тип подкласса", неважно, какой синтаксис должен был бы ссылаться на него в родительском. В Other это будет означать экземпляр Other, но в Sub это может означать экземпляр Sub? Было бы хорошо определить реализацию вашего метода, возвращающего Other в Other, но не в Sub? Если есть два метода, возвращающих A 's, и один из них реализован в Other, а другой в Sub, означает ли это, что тип, определенный в Base, имеет два разных значения/границы/ограничения в одно и то же время? Тогда что происходит, если A ссылается вне этих классов?

Самое близкое, что у нас есть, - это this.type. Я не уверен, было бы теоретически возможно ослабить смысл this.type (или предоставить более расслабленную версию), но как реализовано, это означает очень специфический тип, поэтому конкретным является то, что единственным возвращаемым значением, удовлетворяющим def foo:this.type, является this.

Я хотел бы иметь возможность делать то, что вы предлагаете, но я не уверен, как это будет работать. Представьте себе, что this.type означало... нечто более общее. Что бы это могло быть? Мы не можем просто сказать "любой из определенных типов this", потому что вы не хотели бы иметь значение class Subclass with MyTrait{type A=MyTrait}. Мы могли бы сказать "тип, удовлетворяющий всем типам this", но он запутывается, когда кто-то пишет val a = new Foo with SomeOtherMixin... и я все еще не уверен, что он может быть определен таким образом, который позволил бы реализовать как Other, так и Sub, определенные выше.

Мы стараемся смешивать статические и динамически определенные типы.

В Scala, когда вы говорите class B { type T <: B }, T специфичен для экземпляра, а B является статическим (я использую это слово в смысле статических методов в java). Вы можете сказать class Foo(o:Object){type T = o.type}, а T будет отличаться для каждого экземпляра.... но когда вы пишете type T=Foo, Foo - статически заданный тип класса. Вы могли бы также иметь object Bar и ссылались на некоторые Bar.AnotherType. AnotherType, поскольку он по существу "статический" (хотя и не называется "static" в Scala), не участвует в наследовании в Foo.

Ответ 4

Однако ни одна из них не заставляет реализацию возвращать свой собственный тип. Например, допустимы следующие классы.

Но разве это не нормально? В противном случае это означало бы, что вы не могли бы просто расширить A, чтобы добавить новый метод, например, так как он автоматически нарушит контракт, который вы пытаетесь создать (т.е. Новый класс copy не вернет экземпляр этот класс, но A). Сам факт того, что он может иметь прекрасный класс A, который ломается, как только вы расширяете его, поскольку класс B чувствует себя неловко для меня. Но, честно говоря, я не могу сказать слова о проблемах, которые он вызывает.

UPDATE: подумав об этом немного, я думаю, что это может звучать, если проверка типа ( "return type == most-производный класс" ) была сделана только в конкретных классах и никогда на абстрактных классах или чертах. Я не знаю, каким образом можно кодировать это в системе типа scala.

Тот факт, что я могу это сделать, приводит к тому, что если я делаю копии объектов, единственной информацией которых я являюсь, является то, что они относятся к данному подклассу A

Почему вы не можете просто вернуть Seq[Ca#Self]? Например, с этим изменением, проходящим список из B to createCopies, как ожидается, вернет a Seq[B] (а не только a 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([email protected], [email protected])

scala> val bs2: Seq[B] = createCopies(bs)
bs2: Seq[B] = List([email protected], [email protected])