使用带有某些基类、抽象类或特征的列表参数化的类型类的最佳方法
Posted
技术标签:
【中文标题】使用带有某些基类、抽象类或特征的列表参数化的类型类的最佳方法【英文标题】:Best way to use type classes with list parametrized with some base class, abstract class or trait 【发布时间】:2011-08-31 23:56:53 【问题描述】:我认为用具体的例子来描述问题会更容易。假设我有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]
。 使用类型类实现我的目标的最佳方式是什么?
我试图找到这个问题的解决方案,但不幸的是还没有找到任何好的解决方案。在printList
函数中知道s
是不够的——不知何故,它需要知道列表中每个元素的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]
是否会在创建的代码中显示或如何显示它。
【问题讨论】:
我认为这个问题在这里得到了回答:***.com/questions/7213676/forall-in-scala @Eric:感谢重播。我更新了我的问题 - 似乎这个解决方案引发了另一个问题。 【参考方案1】:最明显的答案是使用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
或者,您知道,停止子类型化并改用类型类。
【讨论】:
感谢您的回复,请原谅我迟到的答复。在大多数情况下,我可以保持类密封,但有时我想让用户能够拥有自己的特征实现。当然,我可以将Show
实例粘贴到 Fruit 特征上,但这正是我想通过使用类型类来避免的。我也想过用类型类来实现一切,但我相信,它不会解决我描述的问题(如果我错了,请纠正我)。您能否提供一个仅使用类型类并解决我描述的问题的示例?
看看一篇名为“Data Types a la Carte”的论文。这是您问题的解决方案,但它牺牲了通用性。在 Scala 中实现这种方法非常困难。以上是关于使用带有某些基类、抽象类或特征的列表参数化的类型类的最佳方法的主要内容,如果未能解决你的问题,请参考以下文章