Лучший способ использования классов типов со списком, параметризованным некоторым базовым классом, абстрактным классом или признаком
Я думаю, что было бы проще описать проблему с конкретным примером. Предположим, что у меня есть иерархия классов 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]
или как это будет показано, в коде, который создает он.
Ответы
Ответ 1
Наиболее очевидным ответом является использование sealed trait Fruit
и Show[Fruit]
. Таким образом, ваши совпадения будут жаловаться во время компиляции, когда совпадение не является исчерпывающим. Конечно, добавление нового типа Fruit
во внешнюю библиотеку будет невозможно, но это присуще природе вещей. Это проблема .
Вы также можете вставить экземпляр Show
по значению Fruit:
trait Fruit { self =>
def show: Show[self.type]
}
case class Apple() extends Fruit { self =>
def show: Show[self.type] = showA
}
Или, знаете, остановите подтипирование и используйте классы типов.