1
votes

I would like to define a trait which uses the type of the concrete class which extends it as a return type in one of its abstract methods. Is this possible in Scala (2.13)? For example, the following doesn't compile because I can't find a way to bind the ConcreteType:

trait Shape
trait Growable {
  def grow() : ConcreteType
}
case class Circle(x : Int, y : Int, size : Int) extends Shape with Growable {
  def grow() : Circle = this.copy(size = size + 1)
}
case class Square(x : Int, y : Int, size : Int) extends Shape with Growable {
  def grow() : Square = this.copy(size = size + 1)
}

I achieved something close with the following code:

trait Shape
trait Growable[T <: Shape] {
  def grow() : T
}
case class Circle(x : Int, y : Int, size : Int) extends Shape with Growable[Circle] {
  def grow() : Circle = this.copy(size = size + 1)
}
case class Square(x : Int, y : Int, size : Int) extends Shape with Growable[Square] {
  def grow() : Square = this.copy(size = size + 1)
}

Then a user of this code would use it like this:

val circle : Circle = Circle(0, 0, 10).grow()
val square : Square = Square(0, 0, 10).grow()
// or
val shapes : Seq[Shape] = List(circle, square).map(_.grow())

I would like to avoid having to pass in the type via generics, that seems redundant. Any ideas for how to accomplish this?

2
I am surprised the amount of "incorrect" answers, given this is a really common question in Scala. This blog post from Rob Norris explains the problem and solution in detail. - TL;DR; The simplest solution is to use F-Bounded polymorphism (which is similar to your solution), but it is not completely safe. The best solution is to use typeclasses instead of subtype polymorphism. - BTW, if you are interested, I wrote a simple article comparing those two - Luis Miguel Mejía Suárez
Note that, Mario's answer includes the typeclasses approach I mentioned. - Luis Miguel Mejía Suárez

2 Answers

2
votes

Consider shapeless lenses approach

import shapeless._

sealed trait Shape
case class Circle(x : Int, y : Int, size : Int) extends Shape
case class Square(x : Int, y : Int, size : Int) extends Shape

implicit val circleLens = lens[Circle].size
implicit val squareLens = lens[Square].size

implicit class GrowableShape[T <: Shape](shape: T) {
  def grow()(implicit shapeLense: Lens[T, Int]): T =
    shapeLense.modify(shape)(_ + 1)
}

Circle(0, 0, 10).grow()
Square(0, 0, 10).grow()

which outputs

res0: Circle = Circle(0,0,11)
res1: Square = Square(0,0,11)

Alternatively consider a typeclass solution using vanilla scala

trait Growable[T <: Shape] {
  def grow(shape: T): T
}

sealed trait Shape
case class Circle(x : Int, y : Int, size : Int) extends Shape
case class Square(x : Int, y : Int, size : Int) extends Shape

implicit val circleGrowable = new Growable[Circle] {
  def grow(shape: Circle): Circle = shape.copy(size = shape.size + 1)
}

implicit val squareGrowable = new Growable[Square] {
  def grow(shape: Square): Square = shape.copy(size = shape.size + 1)
}

implicit class GrowableShape[T <: Shape](shape: T) {
  def grow()(implicit growable: Growable[T]): T =
    growable.grow(shape)
}

Circle(0, 0, 10).grow()
Square(0, 0, 10).grow()

which outputs

res0: Circle = Circle(0,0,11)
res1: Square = Square(0,0,11)
0
votes

In the simplest way, since, scala methods/function result type is covariance in nature i.e. () => Growable is supertype of () => Circle or () => Square, you can simply do as below by explicitly providing concrete type in implementation:

  trait Shape
  trait Growable {
    def grow() : Growable
  }
  case class Circle(x : Int, y : Int, size : Int) extends Shape with Growable {
    def grow() : Circle = this.copy(size = size + 1)
  }
  case class Square(x : Int, y : Int, size : Int) extends Shape with Growable {
    def grow() : Square = this.copy(size = size + 1)
  }