Подтвердить что ты не робот

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

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

4b9b3361

Ответ 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
}

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