Scala逆变 - 现实生活中的例子

时间:2011-12-26 09:52:16

标签: scala computer-science

我理解scala中的协方差和逆变。协方差在现实世界中有很多应用,但除了相同的函数旧例子外,我无法想到任何逆变量应用。

有人可以对contravariance使用的真实世界示例有所了解吗?

4 个答案:

答案 0 :(得分:20)

在我看来,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

现在我要为Order写一个隐含的Seq[Int]

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]的内容:它们都可以使用相同的参数。

反过来怎么样?假设我有一个只比较::(列表的缺点)的方法,它与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,我可以使用上面隐含的相同内容:

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是反变体。

答案 1 :(得分:18)

此示例来自我正在处理的上一个项目。假设您有一个类型类PrettyPrinter[A],它为漂亮打印A类型的对象提供逻辑。现在,如果B >: A(即如果BA的超类)并且您知道如何打印B(即有PrettyPrinter[B]的实例可用)然后你可以使用相同的逻辑来打印A。换句话说,B >: A暗示PrettyPrinter[B] <: PrettyPrinter[A]。因此,您可以在PrettyPrinter[A]上声明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标准库的 Ordering类型类, EqualShow(与上面PrettyPrinter同构),来自Scalaz等的Resource类型类。

修改
正如但以理指出的那样,斯卡拉的Ordering不是逆变的。 (我真的不知道为什么。)您可以考虑将scalaz.Order视为与scala.Ordering相同的目的,但它的类型参数是逆变的。

<强>附录:
超类型 - 子类型关系只是两种类型之间可以存在的一种关系。可能存在许多这样的关系。让我们考虑与函数A相关的两种类型Bf: B => A(即任意关系)。数据类型F[_]被认为是一个逆变函子,如果你可以为它定义一个操作contramap,它可以将B => A类型的函数提升为F[A => B]

需要满足以下法律:

  1. x.contramap(identity) == x
  2. x.contramap(f).contramap(g) == x.contramap(f compose g)
  3. 上面讨论的所有数据类型(ShowEqual等)都是逆变函子。这个属性让我们可以做一些有用的事情,如下图所示:

    假设您将类Candidate定义为:

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

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

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

答案 2 :(得分:4)

基于真实世界事件驱动软件系统的示例。这样的系统基于广泛的事件类别,例如与系统功能相关的事件(系统事件),用户动作产生的事件(用户事件)等。

可能的事件层次结构:

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[ErrorEvent]的{​​{1}}的方法。

通过用不一致性约束替换不变性,Sink[Event]成为Sink[SystemEvent]的子类型。因此,逆向也可以被认为是一种“扩大”的关系,因为类型从更具体到更广泛的“扩展”。

<强>结论

此示例已在my blog

中发现的一系列关于方差的文章中进行了描述

最后,我认为有助于理解背后的理论......

答案 3 :(得分:0)

简短的答案可能会帮助像我这样超级困惑并且不想阅读这些冗长的例子的人:

想象一下,您有2个类AnimalCat,它们扩展了Animal。现在,假设您有一个类型Printer[Cat],其中包含用于打印Cat的功能。你有一个这样的方法:

def print(p: Printer[Cat], cat: Cat) = p.print(cat)

但事实是,由于CatAnimalPrinter[Animal]也应该能够打印Cat,对吧?

好吧,如果Printer[T]的定义类似于Printer[-T],即相反,那么我们可以将Printer[Animal]传递给上面的print函数并使用其功能来打印猫。 / p>

这就是为什么存在方差的原因。例如,来自C#的另一个示例是类IComparer,它也是互变的。为什么?因为我们也应该能够使用Animal比较器来比较Cat