Лучший способ использовать классы типов со списком, параметризованным с некоторым базовым классом, абстрактным классом или признаком - PullRequest
12 голосов
/ 01 сентября 2011

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

1 Ответ

7 голосов
/ 01 сентября 2011

Наиболее очевидный ответ - использовать 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
}

Или, вы знаете, прекратить подтипы и использовать вместо них классы типов.

...