Scala逆变 - 实际应用举例

36

我了解Scala中的协变和逆变。协变在现实世界中有许多应用,但是我无法想到任何逆变的应用实例,除了函数的那些老套例子。

能否有人举出一些使用逆变的真实世界例子?


请参考Daniel Spiewak在https://dev59.com/83RB5IYBdhLWcg3wUFrB的回答。 - sourcedelica
3
因为在现实世界中没有人使用函数?=) - Dan Burton
4个回答

24
在我看来,除了Function之外最简单的两个例子是排序和相等性。然而,在Scala标准库中,第一个不是协变的,第二个甚至不存在。因此,我将使用Scalaz的等效物:OrderEqual

接下来,我需要一些类层次结构,最好是熟悉的,并且这两个概念都必须对它有意义。如果Scala有一个所有数字类型的超级类Number,那就太完美了。不幸的是,它没有这样的东西。

所以我要尝试用集合来做这些例子。为了简单起见,让我们只考虑Seq[Int]List[Int]。显然,List[Int]Seq[Int]的子类型,即List[Int]<:Seq[Int]

那么,我们可以用它做什么呢?首先,让我们编写一个比较两个列表的东西:

def smaller(a: List[Int], b: List[Int])(implicit ord: Order[List[Int]]) =
  if (ord.order(a,b) == LT) a else b

现在我打算为Seq[Int]编写一个隐式的Order

implicit val seqOrder = new Order[Seq[Int]] { 
  def order(a: Seq[Int], b: Seq[Int]) = 
    if (a.size < b.size) LT
    else if (b.size < a.size) GT
    else EQ
}

有了这些定义,我现在可以像这样做:

scala> smaller(List(1), List(1, 2, 3))
res0: List[Int] = List(1)

请注意,我正在请求一个Order[List[Int]],但我传递了一个Order[Seq[Int]]。这意味着Order[Seq[Int]] <: Order[List[Int]]。鉴于Seq[Int] >: List[Int],这只有因为逆变才可能。

下一个问题是:这有什么意义吗?

让我们再考虑一下smaller。我想比较两个整数列表。自然地,任何比较两个列表的方法都可以接受,但是将两个Seq[Int]进行比较的方法的逻辑是什么呢?

请注意在seqOrder的定义中,正在比较的内容成为它的参数。显然,一个List[Int]可以作为一个期望Seq[Int]的参数。由此得出,比较Seq[Int]的方法可以替换比较List[Int]的方法:它们都可以与相同的参数一起使用。

那反过来呢?假设我有一个仅比较::(列表的cons)的方法,它和Nil一起是List的子类型。很明显,我不能使用它,因为smaller可能会收到一个Nil进行比较。由此得出,Order[::[Int]]不能替换Order[List[Int]]

让我们继续讨论相等性,并编写一个方法:

def equalLists(a: List[Int], b: List[Int])(implicit eq: Equal[List[Int]]) = eq.equal(a, b)

由于Order扩展了Equal,因此我可以使用相同的隐式(implicit)如下:

scala> equalLists(List(4, 5, 6), List(1, 2, 3)) // we are comparing lengths!
res3: Boolean = true

这里的逻辑是相同的。任何可以确定两个Seq[Int]是否相同的东西,显然也可以确定两个List[Int]是否相同。由此可知,Equal[Seq[Int]] <: Equal[List[Int]],这是正确的,因为Equal是反变。


有scala.Equiv,但它也不是逆变的。 - psp
在你的“较小”示例中,我可以这样做吗: def smaller(a: List[Int], b: List[Int])(implicit ord: Order[Seq[Int]]) = if (ord.order(a,b) == LT) a else b?而不是使用 Order[List[Int]] 来达到相同的目的吗?为什么要使用逆变? - Chao
@Chao 是的,你可以这样做,但是这样你就无法获得更具体的排序。 - Daniel C. Sobral

19

这个例子来自我之前工作的最后一个项目。假设你有一个类型类 PrettyPrinter[A] 为类型为 A 的对象提供漂亮打印的逻辑。如果 B >: A (即如果 BA 的超类) 并且你知道如何漂亮地打印 B (即有一个可用的 PrettyPrinter[B] 实例),那么你可以使用相同的逻辑来漂亮地打印 A。换句话说,B >: A 意味着 PrettyPrinter[B] <: PrettyPrinter[A]。因此,你可以在 A 上将 PrettyPrinter[A] 声明为逆变的。

scala> trait Animal
defined trait Animal

scala> case class Dog(name: String) extends Animal
defined class Dog

scala> trait PrettyPrinter[-A] {
     |   def pprint(a: A): String
     | }
defined trait PrettyPrinter

scala> def pprint[A](a: A)(implicit p: PrettyPrinter[A]) = p.pprint(a)
pprint: [A](a: A)(implicit p: PrettyPrinter[A])String

scala> implicit object AnimalPrettyPrinter extends PrettyPrinter[Animal] {
     |   def pprint(a: Animal) = "[Animal : %s]" format (a)
     | }
defined module AnimalPrettyPrinter

scala> pprint(Dog("Tom"))
res159: String = [Animal : Dog(Tom)]

一些其他的例子包括来自Scala标准库的Equal, Show(与上面的PrettyPrinter同构),以及Scalaz中的Resource 类型类。

编辑:
正如Daniel指出的,Scala的Ordering不是逆变的。(我真的不知道为什么。)您可以考虑使用scalaz.Order,它旨在与scala.Ordering具有相同的目的,但其类型参数是逆变的。

补充:
超类型-子类型关系只是两种类型之间可能存在的一种关系类型。可能存在许多这样的关系。假设有两种类型AB,它们通过函数f: B => A(即任意关系)相关联。如果您可以为数据类型F[_]定义一个操作contramap,该操作可以将类型为B => A的函数提升到F[A => B],则称数据类型F[_]是逆变函子。

需要满足以下法律:

  1. x.contramap(identity) == x
  2. x.contramap(f).contramap(g) == x.contramap(f compose g)

上面讨论的所有数据类型(Show, Equal等)都是逆变函子。这个属性使我们能够执行有用的操作,例如下面所示的操作:

假设您定义了一个类Candidate

case class Candidate(name: String, age: Int)

您需要一个按照候选人年龄排序的 Order[Candidate]。现在您知道有一个可用的 Order[Int] 实例。 您可以使用 contramap 操作从中获取一个 Order[Candidate] 实例:

val byAgeOrder: Order[Candidate] = 
  implicitly[Order[Int]] contramap ((_: Candidate).age)

9
基于真实事件驱动软件系统的示例。这样的系统基于广泛的事件类别,如与系统功能相关的事件(系统事件),由用户操作生成的事件(用户事件)等。
可能的事件层次结构:
trait Event

trait UserEvent extends Event

trait SystemEvent extends Event

trait ApplicationEvent extends SystemEvent

trait ErrorEvent extends ApplicationEvent

现在,工作在事件驱动系统上的程序员需要找到一种方法来注册/处理系统中生成的事件。他们将创建一个名为“Sink”的特征,用于标记需要在事件触发时通知的组件。
trait Sink[-In] {
  def notify(o: In)
}

由于使用了-符号标记类型参数,Sink类型变为逆变。
通知感兴趣的方发生事件的一种可能的方法是编写一个方法并将相应的事件传递给它。这个方法将假设进行一些处理,然后它将负责通知事件接收器:
def appEventFired(e: ApplicationEvent, s: Sink[ApplicationEvent]): Unit = {
  // do some processing related to the event
  // notify the event sink
  s.notify(e)
}

def errorEventFired(e: ErrorEvent, s: Sink[ErrorEvent]): Unit = {
  // do some processing related to the event
  // notify the event sink
  s.notify(e)
}

几个假设的Sink实现。

trait SystemEventSink extends Sink[SystemEvent]

val ses = new SystemEventSink {
  override def notify(o: SystemEvent): Unit = ???
}

trait GenericEventSink extends Sink[Event]

val ges = new GenericEventSink {
  override def notify(o: Event): Unit = ???
}

编译器接受以下方法调用:
appEventFired(new ApplicationEvent {}, ses)

errorEventFired(new ErrorEvent {}, ges)

appEventFired(new ApplicationEvent {}, ges)

看到这一系列的调用,您会注意到可以使用一个期望 Sink[ApplicationEvent] 的方法来调用一个 Sink[SystemEvent] 甚至是一个 Sink[Event]。同样,您也可以使用一个 Sink[Event] 来调用期望 Sink[ErrorEvent] 的方法。

通过将不变性替换为逆变性约束,Sink[SystemEvent] 成为 Sink[ApplicationEvent] 的子类型。因此,逆变性也可以被视为一种“扩展”关系,因为类型从更具体的类型“扩展”到更通用的类型。

结论

这个例子已经在一系列有关 我的博客 上的变异的文章中进行了描述。

最后,我认为理解其背后的理论也是有帮助的...


3
最佳答案在这里。 - Ali

4
以下是可能帮助到跟我一样很困惑,并且不想读这些冗长例子的人的简短答案: 假设您有两个类 AnimalCatCat 继承自 Animal。现在,假设您有一个类型为 Printer[Cat] 的对象,其中包含打印 Cat 的功能。你有一个像这样的方法: def print(p: Printer[Cat], cat: Cat) = p.print(cat) 但问题是,由于 Cat 是一个 Animal,所以应该也能使用 Printer[Animal] 打印 Cat,对吗?
嗯,如果将 Printer[T] 定义为像 Printer[-T] 这样的逆变形式,那么我们可以将 Printer[Animal] 传递给上述的 print 函数,并使用它的功能来打印 Cat
这就是逆变存在的原因。另一个例子是来自 C# 的 IComparer 类,同样也是逆变的。为什么?因为我们应该能够使用 Animal 比较器来比较 Cat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接