Лучший способ использовать классы типов со списком, параметризованным с каким-либо базовым классом, абстрактным классом или трейтом

Я думаю, что было бы проще описать проблему на конкретном примере. Предположим, у меня есть иерархия классов 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] или как он будет показан, в коде, который его создаёт.

12
задан Community 23 May 2017 в 12:25
поделиться