使用类型类的最佳方法,其中列表使用一些基类,抽象类或特征进行参数化

时间:2011-08-31 23:56:54

标签: scala inheritance typeclass

我认为用具体的例子来描述问题会更容易。假设我有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是否会被展示或是如何展示的,创建它的代码。

1 个答案:

答案 0 :(得分:7)

最明显的答案是使用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
}

或者,你知道,停止子类型化并改为使用类型类。