Search code examples
scalatype-systemscats-effect

Scala Type of Extended Generic Type is not resolved by the Compiler


Consider abstract class Fruit:

trait Fruit
case object Apple extends Fruit
case object Orange extends Fruit

I want to implement a processor service for fruits with generic input and output:

trait FruitProcessorInput[T <: Fruit]
case class AppleProcessorInput(parm: List[Int]) extends FruitProcessorInput[Apple.type]
case class OrangeProcessorInput(parm: List[Int]) extends FruitProcessorInput[Orange.type]

trait FruitProcessorOutput[T <: Fruit]
case class AppleProcessorOutput(parm: List[Int]) extends FruitProcessorOutput[Apple.type]
case class OrangeProcessorOutput(parm: List[Int]) extends FruitProcessorOutput[Orange.type]

trait FruitProcessorService[T <: Fruit] {
  def processFruit(input: FruitProcessorInput[T]): FruitProcessorOutput[T]
}

The problem is I can not have a concrete implementation The following two works:

// THIS COMPILES:
case class AppleProcessorService() extends FruitProcessorService[Apple.type] {
  override def processFruit(input: FruitProcessorInput[Apple.type]): FruitProcessorOutput[Apple.type] = ???
}

// THIS COMPILES:
case class AppleProcessorService() extends FruitProcessorService[Apple.type] {
  override def processFruit(input: FruitProcessorInput[Apple.type]): AppleProcessorOutput = ???
}

But the following does not:

// THIS DOES NOT COMPILE:
case class AppleProcessorService() extends FruitProcessorService[Apple.type] {
  override def processFruit(input: AppleProcessorInput): AppleProcessorOutput = ???
}

Mainly when I use the extended type AppleProcessorInput as a parameter scala does not consider it as type FruitProcessorInput[T] even though it extends FruitProcessorInput[Apple.type] whereas it accepts AppleProcessorOutput for FruitProcessorOutput[Apple.type] when it is used for return. Why it does work when it is return type but not when it is parameter type

I am not sure if scala does not allow this for a good reason or is there a workaround.

The reason I have this structure is to have a generic http4s api that I map with

val routes: HttpRoutes[IO] = Router[IO](
  "/apples"         -> service(appleService),
  "/oranges"        -> service(orangeService)
)

Where service looks like

def service[T <: Fruit](
    fruitService: FruitProcessorService[T]
)(implicit decoder: EntityDecoder[IO, FruitProcessorInput[T]]): HttpRoutes[IO] =
  HttpRoutes.of[IO] {
    case req @ GET -> Root                   =>
      for {
        input    <- req.attemptAs[FruitProcessorInput[T]].fold(err => IO.raiseError(err), out => IO(out)).flatten
        output   <- fruitService.processFruit(input)
        response <- Ok(output.asJson)
      } yield response
  }

Solution

  • The problem is that FruitProcessorService[Apple] must accept any FruitProcessorInput[Apple] otherwise it would break Liskov.

    However, we could further tune the interface to model what you want.
    Please, try with the following changes and let me know if it works.

    // Further refine FruitProcessorService:
    trait FruitProcessorService[T <: Fruit] {
      type Input <: FruitProcessorInput[T]
      type Output <: FruitProcessorOutput[T]
      
      def processFruit(input: Input): Output
    }
    
    // Adapt service to the new interface.
    def service[T <: Fruit](
      fruitService: FruitProcessorService[T]
    )(implicit
      decoder: EntityDecoder[IO, fruitService.Input]
      encoder: EntityEncoder[IO, fruitService.Ouput]
    ): HttpRoutes[IO] =
      HttpRoutes.of[IO] {
        case req @ GET -> Root                   =>
          req.as[fruitService.Input].flatMap { input =>
            Ok(fruitService.processFruit(input))
          }
      }