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