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

ten*_*shi 12 inheritance scala 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
Run Code Online (Sandbox Code Playgroud)

我还有一些我希望向用户展示的水果列表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)
Run Code Online (Sandbox Code Playgroud)

这不会编译,因为List参数化了Fruit,我没有定义任何Show[Fruit].使用类型类实现目标的最佳方法是什么?

我试图找到这个问题的解决方案,但遗憾的是还没找到任何好的解决方案.sprintList功能上知道是不够的- 不知何故,它需要知道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)
    }
}
Run Code Online (Sandbox Code Playgroud)

实际上,在haskell中可以找到非常类似的方法.作为一个例子,我们可以看看Eq实现Maybe:

instance (Eq m) => Eq (Maybe m) where  
    Just x == Just y = x == y  
    Nothing == Nothing = True  
    _ == _ = False  
Run Code Online (Sandbox Code Playgroud)

这个解决方案的一个大问题是,如果我要添加这样的新子类Fruit:

case class Banana extends Fruit

object Banana {
    implicit object BananaShow extends Show[Banana] {
        def show(banana: Banana) = "New banana"
    }
}
Run Code Online (Sandbox Code Playgroud)

并会尝试打印我的篮子:

val basket = List[Fruit](Apple(), Orange(), Banana())

printList(basket)
Run Code Online (Sandbox Code Playgroud)

然后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)
    }
}
Run Code Online (Sandbox Code Playgroud)

但这种解决方案远非完美.想象一下,其他一些库提供了另一种水果,它有自己的字典版本.NewFruitShow如果我试着一起使用它们就会发生冲突.

也许我错过了一些明显的东西?


更新

正如@Eric注意到的,这里还有一个解决方案:Scala中的forall.这真的很有趣.但我发现这个解决方案存在一个问题.

如果我使用ShowBox,那么它将在创建时记住具体类型类.所以我通常用对象和对应的类型类建立列表(所以列表中的字典就是这样).另一方面,scala具有非常好的功能:我可以在当前范围中删除新的implicits,它们将覆盖默认值.所以我可以为类定义替代字符串表示,如:

object CompactShow { 
    implicit object AppleCompactShow extends Show[Apple] {
        def show(apple: Apple) = "SA"
    }

    implicit object OrangeCompactShow extends Show[Orange] {
        def show(orange: Orange) = "SO"
    }
}
Run Code Online (Sandbox Code Playgroud)

然后只需在当前范围内导入它import CompactShow._.在这种情况下AppleCompactShow,OrangeCompactShow将隐式使用对象而不是在伴随对象中定义的默认值Show.正如您所猜测的,列表创建和打印发生在不同的地方.如果我将使用ShowBox,我可能会捕获类型类的默认实例.我想在最后一刻抓住它们 - 我打电话的那一刻printList,因为我甚至不知道List[Fruit],在创建它的代码中我是否会被展示或如何展示.

Apo*_*isp 7

最明显的答案是使用a sealed trait Fruit和a Show[Fruit].这样,当匹配不详尽时,您的模式匹配将在编译时抱怨.当然,Fruit在外部库中添加新类型是不可能的,但这是事物本质所固有的.这是" 表达问题 ".

您还可以将Show实例粘贴在Fruit特征上:

trait Fruit { self =>
  def show: Show[self.type]
}

case class Apple() extends Fruit { self =>
  def show: Show[self.type] = showA
}
Run Code Online (Sandbox Code Playgroud)

或者,您知道,停止子类型并改为使用类型类.