Я думаю, что было бы проще описать проблему на конкретном примере. Предположим, у меня есть иерархия классов 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
для Может быть
:
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)
тогда скала.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, есть еще одно решение, описанное здесь: forall в Скале . Это действительно выглядит очень интересно. Но я вижу одну проблему с этим решением.
Если я использую ShowBox
, то во время создания он запомнит конкретный тип класса. Поэтому я в общем строю список с объектами и классами соответствующих типов (так что словарь в списке присутствует). С другой стороны, скала имеет очень хорошую особенность: Я могу опустить новые имплициты в текущий масштаб, и они будут переопределять значения по умолчанию. Таким образом, я могу определить альтернативное строковое представление для классов типа:
object CompactShow {
implicit object AppleCompactShow extends Show[Apple] {
def show(apple: Apple) = "SA"
}
implicit object OrangeCompactShow extends Show[Orange] {
def show(orange: Orange) = "SO"
}
}
и затем просто импортировать его в текущую область видимости с помощью импорта CompactShow._
. В этом случае объект AppleCompactShow
и OrangeCompactShow
будут неявно использоваться вместо значений по умолчанию, определенных в объекте-сопутствующем Show
. И как вы можете догадаться, создание и распечатка списков происходит в разных местах. Если я буду использовать ShowBox
, то, скорее всего, я захвачу экземпляры класса типа по умолчанию. Хотелось бы перехватить их в последний момент - в момент вызова printList
, потому что я даже не знаю, будет ли когда-нибудь показан мой List[Fruit]
или как он будет показан, в коде, который его создаёт.