Scala - Абстрактные типы и неявное разрешение параметров
Я использую Scala 2.10.4.
Пожалуйста, приведите аналогию - реальный код глубоко внедрен в сложную программу, поэтому вместо того, чтобы объяснять это, я абстрактно расскажу о проблеме, рассказав о Animals;
В scala у меня есть 2 черты - например:
Animal, и HouseBase.
У меня нет доступа к изменению Animal, но я наследую его от таких классов, как Dog, Rabbit, Fish. Досадно, что я не могу изменить каждый подкласс, так как мне не принадлежат все подклассы, которые я использую.
Все мои животные живут где-то - их дома должны наследовать от HouseBase. Я могу изменить HouseBase и его подклассы (через другой уровень абстракции, если я должен).
Таким образом, собака является подклассом Animal и будет жить в питомнике, который является подклассом HouseBase.
Кролик будет жить в клетке, а рыба в аквариуме.
Обратите внимание, что здесь не применяются отношения 1:1 - рыба также может жить в пруду, и мы должны быть в состоянии справиться и с этим.
Я надеялся, что, учитывая конкретное животное (например, Рыбу), на которое ссылается абстрактный тип Animal, и конкретный возвращаемый тип (например, Tank), Scala сможет автоматически выбрать правильный неявный параметр в дизайн у меня ниже.
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
}
Однако есть 2 связанные проблемы.
Проблема 1 - Если на животное ссылаются как на абстрактное, то неявный параметр будет искать только те функции, которые принимают абстрактный тип (Animal), а не базовый конкретный тип. Я подозреваю, что решением может быть использование ClassTags, потому что Scala, кажется, не использует информацию времени выполнения? Я попытался реализовать это и безнадежно заблудился (я довольно новичок в Scala!).
Проблема 2. Если мое животное может жить в нескольких типах домов, возникает аналогичная проблема: даже если указан конкретный тип возвращаемого значения, компилятор найдет два неявных объекта для Fish неоднозначными. Я немного озадачен тем, что здесь делать!
Я могу придумать решения с ручным шаблоном, чтобы соответствовать типу во время выполнения, но это не очень расширяемо.
Любые идеи с благодарностью принимаются! Остальная часть кода ниже.
Изменить - эти ссылки, кажется, подтверждают то, что я подозревал. Этот полиморфизм времени компиляции используется, и, следовательно, тип времени выполнения не может быть известен:
http://like-a-boss.net/2013/03/29/polymorphism-and-typeclasses-in-scala.html
Итак, я полагаю, что мой вопрос сейчас, учитывая это, есть ли способ изменить мой пример, чтобы использовать диспетчеризацию во время выполнения?
Животные:
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
Дома и последствия:
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
}
}
2 ответа
В общем, вы хотите следующий дизайн:
- Во время компиляции конкретный тип
HouseBase
известен. - Во время компиляции конкретный тип
Animal
не известно - Создать определенный тип
HouseBase
заAnimal
предоставленные данные о животных во время выполнения. - Не могу изменить
Animal
реализации, и не очень хочется менятьHouseBase
Реализации.
Желательно, конечно, иметь конкретные типы Animal
Доступно во время компиляции. Поскольку, кажется, есть некоторые знания об этом (вы знаете, какие HouseBase
для создания переменной животного во время компиляции), вы можете попробовать использовать безопасное приведение типов из shapeless
чтобы получить Option
бетона Animal
тип.
Но если это невозможно, вы должны использовать отправку животных во время выполнения.
В этом случае я думаю, что метод create
должна иметь следующую подпись:
def create[HB <: HouseBase](animal: Animal): Option[HB]
Вы знаете конкретный тип HouseBase
так что вы можете также передать его как параметр типа, и возвращаемое значение Option
для учета возможного несоответствия между типом поставляемого животного и подходящими типами животных для конкретного HouseBase
Одним из возможных способов реализации этого является следующий код с одним объектом, который обладает всеми знаниями о создании HouseBase
с из Animal
s (должно быть также возможно достичь того же самого путем перемещения кода создания в сопутствующие объекты бетона HouseBase
s):
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)
}
Затем вы можете вызвать функции следующим образом:
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)
Кроме того, стандартный код в HouseCreator
можно уменьшить с помощью PartialFunction
s:
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)
}
Вы хотите, чтобы компилятор определял тип времени выполнения вашего подкласса, когда он статически объявлен как экземпляр своего суперкласса. Это невозможно, поэтому не пытайтесь заставить это работать, если только вы не надеетесь получить какую-то награду в области компьютерных наук!
Вместо параметризации вашего HouseCreator
класс, вы могли бы написать это, чтобы иметь один create()
метод, который принимает объект типа Animal
, Это может создать соответствующий House
используя совпадение регистра, основанное на подтипе времени выполнения Animal
,
sealed trait HouseCreator {
def create(animal: Animal): HouseBase {
animal match {
case dog: Dog => new Kennel(Seq(dog.boneName))
case fish: Fish => // etc...
}
}
}
Это сможет только вернуть HouseBase
объект, а не конкретный подкласс (по крайней мере, как я реализовал это здесь). Вы также всегда можете сопоставить возвращаемое значение.