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

Dzh*_*zhu 30 computer-science scala

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

有人可以了解现实世界contravariance使用范例吗?

Dan*_*ral 22

在我看来,之后的两个最简单的例子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
Run Code Online (Sandbox Code Playgroud)

现在我要写一个隐含OrderSeq[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
}
Run Code Online (Sandbox Code Playgroud)

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

scala> smaller(List(1), List(1, 2, 3))
res0: List[Int] = List(1)
Run Code Online (Sandbox Code Playgroud)

请注意,我要求一个Order[List[Int]],但我通过了Order[Seq[Int]].这意味着Order[Seq[Int]] <: Order[List[Int]].鉴于此Seq[Int] >: List[Int],这只能因为反差而成为可能.

接下来的问题是:它有意义吗?

让我们smaller再考虑一下.我想比较两个整数列表.当然,比较两个列表的任何东西都是可以接受的,但是比较两个列表可接受的东西的逻辑是什么Seq[Int]

请注意定义比较的内容seqOrder如何成为参数.显然,a List[Int]可以是期望a的参数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)
Run Code Online (Sandbox Code Playgroud)

因为Order扩展Equal,我可以使用它与上面隐含的相同:

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

这里的逻辑是相同的.任何可以判断两者Seq[Int]是否相同的东西显然也可以说明两者List[Int]是否相同.从这一点,可以得出Equal[Seq[Int]] <: Equal[List[Int]],因为这是真正Equal的禁忌变种.


mis*_*tor 18

这个例子来自我正在进行的上一个项目.假设您有一个类型类PrettyPrinter[A],它为漂亮打印类型的对象提供逻辑A.现在,如果B >: A(即,如果B是超类A)并且您知道如何漂亮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)]
Run Code Online (Sandbox Code Playgroud)

其他一些例子是Ordering来自Scala标准库的类型类 Equal,Show(与PrettyPrinter上面同构),ResourceScalaz等类型类.

编辑:
正如丹尼尔指出的那样,斯卡拉Ordering不是逆变的.(我真的不知道为什么.)你可能会考虑scalaz.Order哪个目的是为了相同的目的,scala.Ordering但是它的类型参数是逆变的.

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

需要满足以下法律:

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

您需要Order[Candidate]按年龄对候选人进行排序.现在你知道有一个Order[Int]可用的实例.您可以Order[Candidate]通过以下contramap操作从中获取实例:

val byAgeOrder: Order[Candidate] = 
  implicitly[Order[Int]] contramap ((_: Candidate).age)
Run Code Online (Sandbox Code Playgroud)


Răz*_*scu 8

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

一个可能的事件层次结构:

trait Event

trait UserEvent extends Event

trait SystemEvent extends Event

trait ApplicationEvent extends SystemEvent

trait ErrorEvent extends ApplicationEvent
Run Code Online (Sandbox Code Playgroud)

现在,在事件驱动系统上工作的程序员需要找到一种方法来注册/处理系统中生成的事件。他们将创建一个特征,Sink,用于标记需要在事件触发时得到通知的组​​件。

trait Sink[-In] {
  def notify(o: In)
}
Run Code Online (Sandbox Code Playgroud)

作为用-符号标记类型参数的结果,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)
}
Run Code Online (Sandbox Code Playgroud)

几个假设的 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 = ???
}
Run Code Online (Sandbox Code Playgroud)

编译器接受以下方法调用:

appEventFired(new ApplicationEvent {}, ses)

errorEventFired(new ErrorEvent {}, ges)

appEventFired(new ApplicationEvent {}, ges)
Run Code Online (Sandbox Code Playgroud)

查看这一系列调用,您会注意到可以调用一个需要 aSink[ApplicationEvent]和 aSink[SystemEvent]甚至是 a 的方法Sink[Event]。此外,您可以调用期望Sink[ErrorEvent]带有 a的方法Sink[Event]

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

结论

这个例子已经在我的博客上关于方差的一系列文章中描述过

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

  • 最佳答案在这里 (3认同)