Я думаю, что было бы проще описать проблему на конкретном примере. Предположим, у меня есть Fruit
иерархия классов и Show
класс классов:
trait Fruit
case class Apple extends Fruit
case class Orange extends Fruit
trait Show[T] {
def show(target: T): String
}
object Show {
implicit object AppleShow extends Show[Apple] {
def show(apple: Apple) = "Standard apple"
}
implicit object OrangeShow extends Show[Orange] {
def show(orange: Orange) = "Standard orange"
}
}
def getAsString[T](target: T)(implicit s: Show[T]) = s show target
У меня также есть список фруктов, которые я хотел бы показать пользователю, используя Show
(это моя главная цель в этом вопросе) :
val basket = List[Fruit](Apple(), Orange())
def printList[T](list: List[T])(implicit s: Show[T]) =
list foreach (f => println(s show f))
printList(basket)
Это не скомпилируется, потому что List
параметризован с Fruit
, а я не определил Show[Fruit]
. Как лучше всего достичь своей цели с помощью классов типов?
Я пытался найти решение этой проблемы, но, к сожалению, пока не нашел ни одного приятного. Недостаточно знать s
в printList
функции - каким-то образом нужно знать Show[T]
для каждого элемента списка. Это означает, что для того, чтобы сделать это, нам нужен некоторый механизм времени выполнения в дополнение к механизму времени компиляции. Это дало мне представление о каком-то временном словаре, который знает, как найти корреспондента Show[T]
во время выполнения.
В качестве словаря может служить реализация неявного Show[Fruit]
:
implicit object FruitShow extends Show[Fruit] {
def show(f: Fruit) = f match {
case a: Apple => getAsString(a)
case o: Orange => getAsString(o)
}
}
И на самом деле очень похожий подход можно найти в haskell. В качестве примера мы можем рассмотреть реализацию Eq
для Maybe
:
instance (Eq m) => Eq (Maybe m) where
Just x == Just y = x == y
Nothing == Nothing = True
_ == _ = False
Большая проблема с этим решением заключается в том, что, если я добавлю новый подкласс Fruit
, например:
case class Banana extends Fruit
object Banana {
implicit object BananaShow extends Show[Banana] {
def show(banana: Banana) = "New banana"
}
}
и попробую распечатать мою корзину:
val basket = List[Fruit](Apple(), Orange(), Banana())
printList(basket)
тогда scala.MatchError
будет брошено, потому что мой словарь еще ничего не знает о бананах. Конечно, я могу предоставить обновленный словарь в некотором контексте, который знает о бананах:
implicit object NewFruitShow extends Show[Fruit] {
def show(f: Fruit) = f match {
case b: Banana => getAsString(b)
case otherFruit => Show.FruitShow.show(otherFruit)
}
}
Но это решение далеко от совершенства. Только представьте, что какая-то другая библиотека предоставляет другой фрукт со своей версией словаря. Это просто будет конфликтовать с NewFruitShow
, если я попытаюсь использовать их вместе.
Может быть, я упускаю что-то очевидное?
Обновление
Как заметил @Eric, здесь описано еще одно решение: в Scala . Это действительно выглядит очень интересно. Но я вижу одну проблему с этим решением.
Если я использую ShowBox
, он запомнит конкретный класс типов во время создания. Таким образом, я обычно строю список с объектами и соответствующими типами классов (поэтому словарь присутствует в списке). С другой стороны, у scala есть очень приятная особенность: я могу удалить новые импликации в текущей области видимости, и они переопределят значения по умолчанию. Поэтому я могу определить альтернативное строковое представление для таких классов, как:
object CompactShow {
implicit object AppleCompactShow extends Show[Apple] {
def show(apple: Apple) = "SA"
}
implicit object OrangeCompactShow extends Show[Orange] {
def show(orange: Orange) = "SO"
}
}
, а затем просто импортируйте его в текущую область с import CompactShow._
. В этом случае объект AppleCompactShow
и OrangeCompactShow
будет использоваться неявно вместо значений по умолчанию, определенных в сопутствующем объекте Show
. И, как вы можете догадаться, создание и печать списка происходит в разных местах. Если я буду использовать ShowBox
, то, скорее всего, я буду захватывать экземпляры класса по умолчанию. Я хотел бы запечатлеть их в самый последний момент - момент, когда я звоню printList
, потому что я даже не знаю, будет ли когда-либо отображаться мой List[Fruit]
или как он будет отображаться в коде, который создает это.