我认为用具体的例子来描述问题会更容易。假设我有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
的{{1}}实现:
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)
}
}
冲突。
也许我错过了一些明显的东西?
正如@Eric注意到的,这里还有一个解决方案:forall in Scala。这真的很有趣。但我发现这个解决方案存在一个问题。
如果我使用NewFruitShow
,那么它会在创建时记住具体的类类。所以我通常用对象和对应的类型类建立列表(所以列表中的字典就是这样)。另一方面,scala具有非常好的功能:我可以在当前范围中删除新的implicits,它们将覆盖默认值。所以我可以为类定义替代字符串表示,如:
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"
}
}
将其导入当前范围。在这种情况下,将隐式使用import CompactShow._
和AppleCompactShow
对象,而不是OrangeCompactShow
的伴随对象中定义的默认值。正如您所猜测的,列表创建和打印发生在不同的地方。如果我将使用Show
,那么我很可能会捕获类型类的默认实例。我想在最后一刻抓住它们 - 我打电话给ShowBox
的那一刻,因为我甚至不知道,我的printList
是否会被展示或是如何展示的,创建它的代码。
答案 0 :(得分:7)
最明显的答案是使用sealed trait Fruit
和Show[Fruit]
。这样,当匹配不详尽时,您的模式匹配将在编译时抱怨。当然,在外部库中添加新类型Fruit
是不可能的,但这是事物本质所固有的。这是“expression problem”。
您还可以将Show
实例粘贴在Fruit特征上:
trait Fruit { self =>
def show: Show[self.type]
}
case class Apple() extends Fruit { self =>
def show: Show[self.type] = showA
}
或者,你知道,停止子类型化并改为使用类型类。