使用带有某些基类、抽象类或特征的列表参数化的类型类的最佳方法

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._ 将其导入当前范围。在这种情况下,AppleCompactShowOrangeCompactShow 对象将被隐式使用,而不是在 Show 的伴随对象中定义的默认值。你可以猜到,列表的创建和打印发生在不同的地方。如果我将使用ShowBox,那么我很可能会捕获类型类的默认实例。我想在最后一刻捕捉它们——我打电话给printList 的那一刻,因为我什至不知道我的List[Fruit] 是否会在创建的代码中显示或如何显示它。

【问题讨论】:

我认为这个问题在这里得到了回答:***.com/questions/7213676/forall-in-scala @Eric:感谢重播。我更新了我的问题 - 似乎这个解决方案引发了另一个问题。 【参考方案1】:

最明显的答案是使用sealed trait FruitShow[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 中实现这种方法非常困难。

以上是关于使用带有某些基类、抽象类或特征的列表参数化的类型类的最佳方法的主要内容,如果未能解决你的问题,请参考以下文章

java基础

java编程思想第八章多态

抽象类

java抽象类和接口

Java 面向对象

第二十天:继承