我认为用具体的例子来描述问题会更容易。假设我有 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 所注意到的,这里还描述了另一种解决方案:forall in 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]
在创建它的代码中将永远显示或如何显示。
最佳答案
最明显的答案是使用 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
}
或者,您知道,停止子类型并改用类型类。
关于scala - 使用带有一些基类、抽象类或特征参数化列表的类型类的最佳方法,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/7264824/