Scala: how to write method that returns object typed to implementation type of receiver
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)
精彩评论