我理解scala中的协方差和逆变。协方差在现实世界中有很多应用,但除了相同的函数旧例子外,我无法想到任何逆变量应用。
有人可以对contravariance
使用的真实世界示例有所了解吗?
答案 0 :(得分:20)
在我看来,Function
之后的两个最简单的例子是排序和相等。但是,第一个不是Scala标准库中的反变体,第二个甚至不存在于其中。所以,我将使用Scalaz等价物:Order和Equal。
接下来,我需要一些类层次结构,最好是一个熟悉的层次结构,当然,上面的两个概念都必须有意义。如果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
(即如果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)]
其他一些示例是来自Scala标准库的 Ordering
类型类,Equal
,Show
(与上面PrettyPrinter
同构),来自Scalaz等的Resource
类型类。
修改强>
正如但以理指出的那样,斯卡拉的Ordering
不是逆变的。 (我真的不知道为什么。)您可以考虑将scalaz.Order
视为与scala.Ordering
相同的目的,但它的类型参数是逆变的。
<强>附录:强>
超类型 - 子类型关系只是两种类型之间可以存在的一种关系。可能存在许多这样的关系。让我们考虑与函数A
相关的两种类型B
和f: B => A
(即任意关系)。数据类型F[_]
被认为是一个逆变函子,如果你可以为它定义一个操作contramap
,它可以将B => A
类型的函数提升为F[A => B]
。
需要满足以下法律:
x.contramap(identity)
== x
x.contramap(f).contramap(g)
== x.contramap(f compose g)
上面讨论的所有数据类型(Show
,Equal
等)都是逆变函子。这个属性让我们可以做一些有用的事情,如下图所示:
假设您将类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个类Animal
和Cat
,它们扩展了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
函数并使用其功能来打印猫。 / p>
这就是为什么存在方差的原因。例如,来自C#
的另一个示例是类IComparer
,它也是互变的。为什么?因为我们也应该能够使用Animal
比较器来比较Cat
。