Search code examples
scalareflectionpolymorphismerasureimplicit-parameters

Scala - Abstract types and Implicit Parameter Resolution


I'm using Scala 2.10.4.

Please bare with the analogy - the actual code is deeply embedded in a complicated program, so rather than explain that, I’ll abstract the problem in a time honoured way to talk about Animals ;-)

In scala I have 2 traits - for example:

Animal, and HouseBase.

I have no access to change Animal, but I inherit from it with classes like Dog, Rabbit, Fish. Annoyingly I can’t change every subclass as I don’t own all the subclasses I use.

My animals all live somewhere - their homes must inherit from HouseBase. I can change HouseBase and it’s subclasses (via another layer of abstraction, if I must).

So a Dog is a subclass of Animal, and would live in a Kennel which is a subclass of HouseBase.

A Rabbit would live in a Hutch, and a Fish in a Tank.

Note there is not a 1:1 relationship enforced here - a Fish could also live in a Pond, and we’d have to be able to handle that too.

What I’d hoped was that -- given a concrete animal (eg Fish), that is referenced via an abstract type Animal, and given a concrete return type (eg Tank), Scala would be able to automagically pick the correct implicit parameter in the design I have below.

object AnimalSelectionProblem extends App {

  def abstractFish : Animal = new Fish(true, 20.0)
  def concreteFish : Fish = new Fish(false, 30.0)
  def abstractDog : Animal = new Dog("tasty bone")
  def concreteDog : Dog = new Dog("yummy bone")
  def abstractRabbit : Animal = new Rabbit(5)
  def concreteRabbit : Rabbit = new Rabbit(10)

  import HouseImplicits._

  val myTank1: Tank = HouseImplicits.create(abstractFish)
  val myTank2: Tank = HouseImplicits.create(concreteFish)

  val myKennel1: Kennel = HouseImplicits.create(abstractDog)
  val myKennel2: Kennel = HouseImplicits.create(concreteDog) // This works

  val myhutch1: Hutch = HouseImplicits.create(abstractRabbit)
  val myhutch2: Hutch = HouseImplicits.create(concreteRabbit) // This works

}

However there are 2 related problems.

Problem 1 - If the animal is referenced as an abstract, then the implicit parameter will only look for functions that take the abstract type (Animal) rather than the underlying concrete type. I suspect the solution may be to use ClassTags, because Scala doesn’t seem to use runtime time information? I had a go at implementing this and got hopelessly l lost (I’m fairly new to Scala!).

Problem 2 - If my animal can live in more than one type of House then a similar issue occurs, that even if a concrete return type is specified, the compiler will find the 2 implicit objects for Fish ambiguous. I’m a bit stumped about what to do here!

I can dream up solutions with manual boilerplate to match the type at runtime, but this isn’t very extensible.

Any ideas gratefully received! Rest of the code is below.

Edit - these links seems to confirm what I had suspected. That compile time polymorphism is used and hence the runtime type cannot be known:

http://like-a-boss.net/2013/03/29/polymorphism-and-typeclasses-in-scala.html

https://softwareengineering.stackexchange.com/questions/258698/is-it-possible-to-have-ad-hoc-polymorphism-with-runtime-dispatch

So, I guess my question now is, given this, is there a way to modify my example to use runtime dispatch?

Animals:

trait Animal {

}

class Dog(val boneName: String) extends Animal
class Rabbit(val length: Int) extends Animal
class Fish(val likesFrogs: Boolean, val optimumTemp: Double) extends Animal

Houses and Implicits:

sealed trait HouseBase

// Made up some arbitrary member variables
case class Kennel(posessions: Seq[String]) extends HouseBase
case class Hutch(length: Int) extends HouseBase
case class Tank(waterTemp: Double) extends HouseBase
case class Pond(containsFrog: Boolean) extends HouseBase

sealed trait HouseCreator[A <: Animal, HB <: HouseBase] {
  def create(animal: A): HB
}

object HouseImplicits {

  implicit object BuildKennelForDog extends HouseCreator[Dog, Kennel] {
    override def create(dog: Dog): Kennel = {
      new Kennel(Seq(dog.boneName))
    }
  }

  implicit object BuildTankForFish extends HouseCreator[Fish, Tank] {
    override def create(fish: Fish): Tank = {
      new Tank(fish.optimumTemp)
    }
  }

  implicit object BuildPondForFish extends HouseCreator[Fish, Pond] {
    override def create(fish: Fish): Pond = {
      new Pond(fish.likesFrogs)
    }
  }

  implicit object BuildHutchForRabbit extends HouseCreator[Rabbit, Hutch] {
    override def create(rabbit: Rabbit): Hutch = {
      new Hutch(rabbit.length*5)
    }
  }

  def create[A <: Animal, H <: HouseBase](animal: A)(implicit house: HouseCreator[A,H]) : H = {
    val newHouse = house.create(animal)
    newHouse
  }
}

Solution

  • So basically you want the following design:

    • At compile time the concrete type of HouseBase is known.
    • At compile time the concrete type of Animal is not known.
    • Create a specific type HouseBase for Animal provided runtime animal data.
    • Can't change Animal implementations, and don't really want to change HouseBase implementations.

    The desirable thing is of course to have the concrete types of Animals available at compile time. Since there seems to be some knowledge of that (you know which HouseBase to create for an animal variable at compile time), you may try to use type-safe cast from shapeless to get an Option of a concrete Animal type.

    But if it's not possible you have to use run-time dispatch of animals.

    In that case I think the method create should have the following signature:

    def create[HB <: HouseBase](animal: Animal): Option[HB]
    

    You know the concrete type of the HouseBase so you may as well pass it as type parameter, and the return value is Option to account for a possible mismatch between the type of the provided animal and suitable animal types for a concrete HouseBase

    One possible way to implement that is the following code with a single object that has all the knowledge about the production of HouseBases from Animals (it should be also possible to achieve the same thing by moving the creation code into companion objects of concrete HouseBases):

    sealed trait HouseCreator[HB <: HouseBase] {
      def create(animal: Animal): Option[HB]
    }
    
    object HouseCreator {
      implicit object KennelCreator extends HouseCreator[Kennel] {
        def create(animal: Animal): Option[Kennel] = animal match {
          case dog: Dog => Some(Kennel(Seq(dog.boneName)))
          case _ => None
        }
      }
    
      implicit object HutchCreator extends HouseCreator[Hutch] {
        def create(animal: Animal): Option[Hutch] = animal match {
          case rabbit: Rabbit => Some(Hutch(rabbit.length * 5))
          case _ => None
        }
      }
    
      implicit object TankCreator extends HouseCreator[Tank] {
        def create(animal: Animal): Option[Tank] = animal match {
          case fish: Fish => Some(Tank(fish.optimumTemp))
          case _ => None
        }
      }
    
      implicit object PondCreator extends HouseCreator[Pond] {
        def create(animal: Animal): Option[Pond] = animal match {
          case fish: Fish => Some(Pond(fish.likesFrogs))
          case _ => None
        }
      }
    
      def create[HB <: HouseBase : HouseCreator](animal: Animal): Option[HB] =
        implicitly[HouseCreator[HB]].create(animal)
    }
    

    Then you can call the functions this way:

    val myTank1: Option[Tank] = HouseCreator.create[Tank](abstractFish)
    val myTank2: Option[Tank] = HouseCreator.create[Tank](concreteFish)
    
    // Types of the variables can also be inferred automatically
    val myKennel1 = HouseCreator.create[Kennel](abstractDog)
    val myKennel2 = HouseCreator.create[Kennel](concreteDog)
    
    val myhutch1 = HouseCreator.create[Hutch](abstractRabbit)
    val myhutch2 = HouseCreator.create[Hutch](concreteRabbit)
    

    Also, the boilerplate code in HouseCreator can be reduced by using PartialFunctions:

    sealed trait HouseCreator[HB <: HouseBase] {
      def create: PartialFunction[Animal, HB]
    }
    
    object HouseCreator {
      implicit object KennelCreator extends HouseCreator[Kennel] {
        def create = {
          case dog: Dog => Kennel(Seq(dog.boneName))
        }
      }
    
      implicit object HutchCreator extends HouseCreator[Hutch] {
        def create = {
          case rabbit: Rabbit => Hutch(rabbit.length * 5)
        }
      }
    
      implicit object TankCreator extends HouseCreator[Tank] {
        def create = {
          case fish: Fish => Tank(fish.optimumTemp)
        }
      }
    
      implicit object PondCreator extends HouseCreator[Pond] {
        def create = {
          case fish: Fish => Pond(fish.likesFrogs)
        }
      }
    
      def create[HB <: HouseBase : HouseCreator](animal: Animal): Option[HB] =
        implicitly[HouseCreator[HB]].create.lift(animal)
    }