如何检查函数中元素的协变和逆变位置?

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 对其应用水果,并从中接收水果。由于sf 的子类型,这意味着我们可以提供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 的输入。因此,您的类不能是协变的

唯一剩下的可能性是:PetsA 中必须是不变的。


###插图:协方差与逆变:

我将利用这个机会展示一个改进的和显着的更多 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" 

BrowseYoutubeEntertainment 的最通用来源:它可以为您提供 基本上任何类型的娱乐:猫视频,杂耍小丑,或(意外) 一些音乐。 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 你为什么选择逆变示例作为 t​​rait Consumer[-A] def consume(a: A): Unit 和协变示例作为:trait Producer[+A] def get:一个

以上是关于如何检查函数中元素的协变和逆变位置?的主要内容,如果未能解决你的问题,请参考以下文章

Typescript中的协变和逆变

Java泛型中的协变和逆变

Typescript中的协变和逆变

Typescript中的协变和逆变

Typescript中的协变和逆变

了解 C# 中的协变和逆变接口