开发者

Scala: how to write method that returns object typed to implementation type of receiver

开发者 https://www.devze.com 2023-03-19 20:46 出处:网络
I\'m aware that case class inheritance is deprecated in Scala, but for the sake of simplicity, I\'ve used it in the following example:

I'm aware that case class inheritance is deprecated in Scala, but for the sake of simplicity, I've used it in the following example:

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 far, so good. What I really want, though, is to be able to write a method foo() in Foo that returns an object of type Bar when called on an object of type B开发者_如何学Car, without having to reimplement the method in class Bar. Is there a way to do this in Scala?


builder approach

Yes, it can be done. A good example of that is the collections library.

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)

See The Architecture of Scala Collections to see how it was done.

Edit: It uses two approaches to reuse implementations. The first is by using common traits and builders, and the other is using type classes.

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

The whole point of doing this, is so you can do something interesting at the common trait, like implementing common map in FooLike. It's hard to see the benefits with trivial code.

type class approach

The benefit of using a type class is that you can add features to Foo and Bar even when you can't change them (like 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


The copy method is implemented by the compiler and it does not seem to belong a common trait. The easiest way to do it is to define a trait:

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)

Another option is to use type classes to provide an appropriate builder. But it wont save the number of typed characters.


Note: This does not create a new object but re-uses the this object. For general use, see paradigmatic’s answer.

For some reason, it does not work together with the case class’s copy method. (But admittedly, since case class inheritance should not be done anyway, the problem does not occur.). But for any other method, you do it with 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)

If you need the self-type variance in method arguments and method bodys (as opposed to return-type-only variance), you will need to go one step further and define

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

This will allow you to add proper method bodies to the trait. (See: proper class hierarchy for 2D and 3D vectors)


This solution doesn't require a separate trait.

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

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号