Я думаю, что было бы проще описать проблему с конкретным примером. Предположим, что у меня есть иерархия классов Fruit
и Show
type class:
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
, если я попытаюсь использовать их вместе.
Может, мне что-то не хватает?
Update
Как заметил @Eric, есть еще одно решение, описанное здесь: forall в 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]
или как это будет показано, в коде, который создает он.