如何检查函数中元素的协变和逆变位置?
Posted
技术标签:
【中文标题】如何检查函数中元素的协变和逆变位置?【英文标题】:How to check covariant and contravariant position of an element in the function? 【发布时间】:2018-07-26 12:08:23 【问题描述】:这是我读过的一篇关于 scala 中的逆变和协变的文章中的代码 sn-p。但是,我无法理解scala编译器抛出的错误消息“错误:协变类型A发生在值pet2的类型A中的逆变位置
class Pets[+A](val pet:A)
def add(pet2: A): String = "done"
我对这段代码sn-p的理解是Pets是协变的,并且接受A的子类型的对象。但是,函数add只接受A类型的参数。协变意味着Pets可以接受Type的参数A 及其亚型。那么这应该如何引发错误。哪里会出现逆变问题。
对上述错误消息的任何解释都将非常有帮助。谢谢
【问题讨论】:
【参考方案1】:Pets
类在其类型 A 中是 协变(因为它被标记为 +A),但您在 逆变 位置使用它。这是因为,如果你看一下 Scala 中的 Function trait,你会发现输入参数类型是逆变的,而返回类型是协变的。 每个函数的输入类型都是逆变的,返回类型是协变的。
例如,接受一个参数的函数有这样的定义:
trait Function1[-T1, +R]
问题是,要使函数S
成为函数F
的子类型,它需要“要求(相同或)更少并提供(相同或)更多”。这也被称为 Liskov 替换原则。在实践中,这意味着 Function trait 需要在其输入中是逆变的,在其输出中是协变的。通过在输入中逆变,它需要“相同或更少”,因为它接受T1
或其任何超类型(这里“更少”表示“超类型”,因为我们正在放松限制,例如从水果到食物)。此外,由于它的返回类型是协变的,它需要“相同或更多”,这意味着它可以返回R
或比这更具体的任何东西(这里“更多”意味着“子类型”,因为我们正在添加更多信息,例如来自水果到苹果)。
但是为什么呢?为什么不反过来呢?这是一个希望能更直观地解释它的示例 - 想象两个具体函数,一个是另一个的子类型:
val f: Fruit => Fruit
val s: Food => Apple
函数s
是函数f
的有效子类型,因为它需要更少(我们“丢失”从水果到食物的信息)并提供更多(我们“获得”从水果到苹果的信息)。注意s
的输入类型是f
的输入类型(逆变)的超类型,它的返回类型是f
的返回类型(协方差)的子类型。现在让我们想象一段使用这些函数的代码:
def someMethod(fun: Fruit => Fruit) = // some implementation
someMethod(f)
和 someMethod(s)
都是有效的调用。方法someMethod
在内部使用fun
对其应用水果,并从中接收水果。由于s
是f
的子类型,这意味着我们可以提供Food => Apple
作为fun
的完美实例。 someMethod
中的代码会在某个时候为 fun
提供一些水果,这没关系,因为 fun
需要食物,而水果是食物。另一方面,fun
具有 Apple
作为返回类型也很好,因为 fun
应该返回水果,并且通过返回苹果符合该约定。
我希望我设法澄清了一点,请随时提出更多问题。
【讨论】:
这个函数“def add(pet2:A):String”应该如何修改才能消除编译错误 @ChaitanyaWaikar 取决于你想要做什么......我总是宁愿远离方差并定义class Pets[A]
,但如果这不是一个选项,你可以通过@987654349 来规避它@.【参考方案2】:
TL;DR:
您的Pets
类可以产生类型为A
的值,方法是返回成员变量pet
,因此Pet[VeryGeneral]
不能是Pet[VerySpecial]
的子类型,因为当它产生VeryGeneral
的东西,它不能保证它也是 VerySpecial
的一个实例。因此,它不可能是逆变的。
您的Pets
类可以使用类型为A
的值,方法是将它们作为参数传递给add
。因此Pet[VerySpecial]
不能是宠物Pet[VeryGeneral]
的子类型,因为它会阻塞任何不是VerySpecial
的输入。因此,您的类不能是协变的。
唯一剩下的可能性是:Pets
在A
中必须是不变的。
###插图:协方差与逆变:
我将利用这个机会展示一个改进的和显着的更多 this comic 的严格版本。它是协方差和逆变的说明 具有子类型和声明站点差异注释的编程语言的概念 (显然,即使是 Java 人也觉得它很有启发性, 尽管问题是关于使用地点的差异)。
首先,插图:
现在使用可编译的 Scala 代码进行更详细的描述。
###逆变的解释(图1左侧)
考虑以下能源的层次结构,从非常一般到非常具体:
class EnergySource
class Vegetables extends EnergySource
class Bamboo extends Vegetables
现在考虑一个具有单个 consume(a: A)
方法的特征 Consumer[-A]
:
trait Consumer[-A]
def consume(a: A): Unit
让我们实现这个特性的几个例子:
object Fire extends Consumer[EnergySource]
def consume(a: EnergySource): Unit = a match
case b: Bamboo => println("That's bamboo! Burn, bamboo!")
case v: Vegetables => println("Water evaporates, vegetable burns.")
case c: EnergySource => println("A generic energy source. It burns.")
object GeneralistHerbivore extends Consumer[Vegetables]
def consume(a: Vegetables): Unit = a match
case b: Bamboo => println("Fresh bamboo shoots, delicious!")
case v: Vegetables => println("Some vegetables, nice.")
object Panda extends Consumer[Bamboo]
def consume(b: Bamboo): Unit = println("Bamboo! I eat nothing else!")
现在,为什么 Consumer
必须在 A
中是逆变的?让我们尝试实例化
几种不同的能源,然后将它们提供给不同的消费者:
val oilBarrel = new EnergySource
val mixedVegetables = new Vegetables
val bamboo = new Bamboo
Fire.consume(bamboo) // ok
Fire.consume(mixedVegetables) // ok
Fire.consume(oilBarrel) // ok
GeneralistHerbivore.consume(bamboo) // ok
GeneralistHerbivore.consume(mixedVegetables) // ok
// GeneralistHerbivore.consume(oilBarrel) // No! Won't compile
Panda.consume(bamboo) // ok
// Panda.consume(mixedVegetables) // No! Might contain sth Panda is allergic to
// Panda.consume(oilBarrel) // No! Pandas obviously cannot eat crude oil
结果是:Fire
可以消费 GeneralistHerbivore
可以消费的所有东西,
反过来GeneralistHerbivore
可以吃掉Panda
可以吃的所有东西。
因此,只要我们只关心消耗能源的能力,
Consumer[EnergySource]
可以在需要 Consumer[Vegetables]
的地方替换,
和
Consumer[Vegetables]
可以在需要 Consumer[Bamboo]
的地方替换。
因此,Consumer[EnergySource] <: Consumer[Vegetables]
和
Consumer[Vegetables] <: Consumer[Bamboo]
,虽然之间的关系
类型参数正好相反:
type >:>[B, A] = A <:< B
implicitly: EnergySource >:> Vegetables
implicitly: EnergySource >:> Bamboo
implicitly: Vegetables >:> Bamboo
implicitly: Consumer[EnergySource] <:< Consumer[Vegetables]
implicitly: Consumer[EnergySource] <:< Consumer[Bamboo]
implicitly: Consumer[Vegetables] <:< Consumer[Bamboo]
###协方差解释(图1右侧)
定义产品的层次结构:
class Entertainment
class Music extends Entertainment
class Metal extends Music // yes, it does, seriously^^
定义一个可以产生A
类型值的特征:
trait Producer[+A]
def get: A
定义不同专业化水平的各种“来源”/“生产者”:
object BrowseYoutube extends Producer[Entertainment]
def get: Entertainment = List(
new Entertainment override def toString = "Lolcats" ,
new Entertainment override def toString = "Juggling Clowns" ,
new Music override def toString = "Rick Astley"
)((System.currentTimeMillis % 3).toInt)
object RandomMusician extends Producer[Music]
def get: Music = List(
new Music override def toString = "...plays Mozart's Piano Sonata no. 11" ,
new Music override def toString = "...plays BBF3 piano cover"
)((System.currentTimeMillis % 2).toInt)
object MetalBandMember extends Producer[Metal]
def get = new Metal override def toString = "I"
BrowseYoutube
是Entertainment
的最通用来源:它可以为您提供
基本上任何类型的娱乐:猫视频,杂耍小丑,或(意外)
一些音乐。
Entertainment
的通用来源由图 1 中的原型小丑表示。
RandomMusician
已经更专业了,至少我们知道这个对象
制作音乐(尽管对任何特定流派没有限制)。
最后,MetalBandMember
非常专业:get
方法保证返回
只有非常具体的 Metal
音乐。
让我们尝试从这三个对象中获取各种Entertainment
:
val entertainment1: Entertainment = BrowseYoutube.get // ok
val entertainment2: Entertainment = RandomMusician.get // ok
val entertainment3: Entertainment = MetalBandMember.get // ok
// val music1: Music = BrowseYoutube.get // No: could be cat videos!
val music2: Music = RandomMusician.get // ok
val music3: Music = MetalBandMember.get // ok
// val metal1: Metal = BrowseYoutube.get // No, probably not even music
// val metal2: Metal = RandomMusician.get // No, could be Mozart, could be Rick Astley
val metal3: Metal = MetalBandMember.get // ok, because we get it from the specialist
我们看到所有三个Producer[Entertainment]
、Producer[Music]
和Producer[Metal]
都可以产生某种Entertainment
。
我们看到只有Producer[Music]
和Producer[Metal]
保证产生Music
。
最后,我们看到只有极其专业的Producer[Metal]
才能保证
产生Metal
,仅此而已。因此,Producer[Music]
和 Producer[Metal]
可以替换
对于Producer[Entertainment]
。 Producer[Metal]
可以替换为Producer[Music]
。
一般来说,生产者
可以用一种更具体的产品代替不那么专业的生产商:
implicitly: Metal <:< Music
implicitly: Metal <:< Entertainment
implicitly: Music <:< Entertainment
implicitly: Producer[Metal] <:< Producer[Music]
implicitly: Producer[Metal] <:< Producer[Entertainment]
implicitly: Producer[Music] <:< Producer[Entertainment]
产品之间的子类型关系与产品之间的子类型关系相同 产品的生产者。这就是协方差的意思。
相关链接
关于 Java 8 中 ? extends A
和 ? super B
的类似讨论:
Java 8 Comparator
comparing()
static function
经典“在我自己的 Either
实现中,flatMap
的正确类型参数是什么”问题:Type L
appears in contravariant position in Either[L, R]
【讨论】:
在阅读了您的示例之后,是的,这是非常清晰和清晰的,因为网络上有成千上万的类似苹果香蕉示例的博客。我唯一不明白的是:\n 你为什么选择逆变示例作为 trait Consumer[-A] def consume(a: A): Unit 和协变示例作为:trait Producer[+A] def get:一个以上是关于如何检查函数中元素的协变和逆变位置?的主要内容,如果未能解决你的问题,请参考以下文章