Frage Scala: Wie man eine Methode schreibt, die das Objekt in den Implementierungstyp des Empfängers zurückgibt


Ich bin mir bewusst, dass die Vererbung von Fallklassen in Scala veraltet ist, aber der Einfachheit halber habe ich sie im folgenden Beispiel verwendet:

scala> case class Foo(val f: String) { def foo(g: String): Foo = { this.copy(f=g) }}
defined class Foo

scala> case class Bar(override val f: String) extends Foo(f)
warning: there were 1 deprecation warnings; re-run with -deprecation for details
defined class Bar

scala> Bar("F")
res0: Bar = Foo(F)

scala> res0.foo("G")
res1: Foo = Foo(G)

So weit, ist es gut. Aber ich möchte wirklich eine Methode schreiben können foo() in Foo, das ein Objekt vom Typ Bar zurückgibt, wenn es für ein Objekt vom Typ Bar aufgerufen wird, ohne die Methode in Klasse Bar neu implementieren zu müssen. Gibt es eine Möglichkeit, dies in Scala zu tun?


5
2017-07-11 08:36


Ursprung


Antworten:


Hinweis: Dies erstellt kein neues Objekt, sondern wiederverwendet das this Objekt. Für den allgemeinen Gebrauch, siehe paradigmatische Antwort.

Aus irgendeinem Grund funktioniert es nicht zusammen mit dem case classIst es copy Methode. (Aber zugegebenermaßen seit case class Vererbung sollte nicht durchgeführt werden, das Problem tritt nicht auf.). Aber für jede andere Methode machst du es mit this.type.

case class Foo(val f: String) { def foo(g: String): this.type = { this }}

case class Bar(override val f: String) extends Foo(f)

Bar("F").foo("G")
res: Bar = Foo(F)

Wenn Sie die Varianz des Typs "self-type" in Methodenargumenten und Methodenkörpern benötigen (im Gegensatz zur reinen Varianz vom Typ Return-only), müssen Sie einen Schritt weiter gehen und definieren

trait HasFoo[T <: HasFoo[T]] { this: T =>
  def foo(g:String): T
  def bar(g: T): T // here may follow an implementation
}

Auf diese Weise können Sie der Methode die richtigen Methodenkörper hinzufügen trait. (Sehen: richtige Klassenhierarchie für 2D- und 3D-Vektoren)


3
2017-07-11 09:15



Builder-Ansatz

Ja, es kann gemacht werden. Ein gutes Beispiel dafür ist die Sammlungsbibliothek.

scala> List(1, 2, 3) take 2
res1: List[Int] = List(1, 2)

scala> Array(1, 2, 3) take 2
res2: Array[Int] = Array(1, 2)

Sehen Die Architektur der Scala-Sammlungen um zu sehen, wie es gemacht wurde.

Bearbeiten: Es verwendet zwei Ansätze, um Implementierungen erneut zu verwenden. Die erste besteht darin, gemeinsame Merkmale und Builder zu verwenden, während die andere Typklassen verwendet.

scala> :paste
// Entering paste mode (ctrl-D to finish)

trait Builder[A] {
  def apply(f: String): A
}
trait FooLike[A] {
  def builder: Builder[A]
  def f: String
  def genericCopy(f: String): A = builder(f)
  def map(fun: String => String): A = builder(fun(f))
}
case class Foo(f: String) extends FooLike[Foo] {
  def builder = new Builder[Foo] {
    def apply(f: String): Foo = Foo(f)
  }
}
case class Bar(f: String) extends FooLike[Bar] {
  def builder = new Builder[Bar] {
    def apply(f: String): Bar = Bar(f)
  }
}

scala> Foo("foo").genericCopy("something")
res0: Foo = Foo(something)

scala> Bar("bar").genericCopy("something")
res1: Bar = Bar(something)

scala> Foo("foo") map { _ + "!" }
res2: Foo = Foo(foo!)

Der springende Punkt dabei ist, dass Sie am gemeinsamen Merkmal etwas Interessantes tun können map im FooLike. Es ist schwer, die Vorteile mit trivialem Code zu sehen.

Typ Klassenansatz

Der Vorteil der Verwendung einer Typklasse besteht darin, dass Sie Funktionen hinzufügen können Foo und Bar auch wenn Sie sie nicht ändern können (wie String).

scala> :paste
// Entering paste mode (ctrl-D to finish)

case class Foo(f: String)
case class Bar(f: String)
trait CanCopy[A] {
  def apply(self: A, f: String): A
  def f(self: A): String
}
object CanCopy {
  implicit val fooCanCopy = new CanCopy[Foo] {
    def apply(v: Foo, f: String): Foo = v.copy(f = f)
    def f(v: Foo) = v.f
  }
  implicit val barCanCopy = new CanCopy[Bar] {
    def apply(v: Bar, f: String): Bar = v.copy(f = f)
    def f(v: Bar) = v.f
  }
  implicit val stringCanCopy = new CanCopy[String] {
    def apply(v: String, f: String): String = f
    def f(v: String) = v
  }

  def copy[A : CanCopy](v: A, f: String) = {
    val can = implicitly[CanCopy[A]]
    can(v, f)
  }

  def f[A : CanCopy](v: A) = implicitly[CanCopy[A]].f(v)
}

scala> CanCopy.copy(Foo("foo"), "something")
res1: Foo = Foo(something)

scala> CanCopy.f(Foo("foo"))
res2: String = foo

scala> CanCopy.copy(Bar("bar"), "something")
res3: Bar = Bar(something)

scala> CanCopy.copy("string", "something")
res4: java.lang.String = something

5
2017-07-11 09:14



Die Kopiermethode wird vom Compiler implementiert und scheint kein gemeinsames Merkmal zu sein. Am einfachsten ist es, ein Merkmal zu definieren:

trait HasFoo[T] {
  def foo(g:String): T
}

case class Foo( f: String ) extends HasFoo[Foo] {
  def foo( g: String ) = copy(f=g)
}

case class Bar( f: String ) extends HasFoo[Bar] {
  def foo( g: String ) = copy(f=g)
}

scala> Bar("a").foo("b")
res7: Bar = Bar(b)

scala> Foo("a").foo("b")
res8: Foo = Foo(b)

Eine andere Option besteht darin, Typklassen zu verwenden, um einen geeigneten Builder bereitzustellen. Es speichert jedoch nicht die Anzahl der eingegebenen Zeichen.


4
2017-07-11 09:20



Diese Lösung erfordert kein separates Merkmal.

class Bar

class Foo {
  def returnMyType[A](x:A) :A = { println(x); x }
}

val f = new Foo
val b = new Bar

val bReturned = f.returnMyType(b)

println(bReturned.getClass.getName)

-3
2017-07-11 09:34