Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

scala

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 Bar, without having to reimplement the method in class Bar. Is there a way to do this in Scala?

like image 320
enhanced_null Avatar asked Jul 11 '11 08:07

enhanced_null


3 Answers

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
like image 99
Eugene Yokota Avatar answered Sep 20 '22 10:09

Eugene Yokota


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.

like image 28
paradigmatic Avatar answered Sep 22 '22 10:09

paradigmatic


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)

like image 25
Debilski Avatar answered Sep 19 '22 10:09

Debilski