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

How to check covariant and contravariant position of an element in the function?

这是我阅读的一篇关于 Scala 中的逆变和协变的文章中的代码片段。但是,我无法理解 scala 编译器抛出的错误消息“error: covariant type A occurs in contravariant position in type A of value pet2

class Pets[+A](val pet:A) {
  def add(pet2: A): String = "done"
}

我对这段代码的理解是,Pets 是协变的,接受 A 的子类型的对象。但是,函数 add 接受类型 A 的参数 only.Being 协变意味着 Pets 可以接受参数A型及其亚型。那么这应该如何抛出错误。逆变的问题从何而来

对上述错误消息的任何解释都将非常有帮助。谢谢

Class Pets 在其类型 A 中是 协变的 (因为它被标记为 +A),但是您在 逆变位置。这是因为,如果您查看 Scala 中的 Function 特征,您会发现输入参数类型是逆变的,而 return 类型是协变的。 每个函数在其输入类型上都是逆变的,在其 return 类型上是协变的

例如,接受一个参数的函数有这样的定义:

trait Function1[-T1, +R]

事实是,要使函数 S 成为函数 F 的子类型,它需要 "require (same or) less and provide (same or) more"。这也称为 Liskov 替换原则。在实践中,这意味着 Function 特征需要在其输入中是逆变的,在其输出中是协变的。通过在其输入中是逆变的,它需要 "same or less",因为它接受 T1 或其任何超类型(这里 "less" 意味着 "supertype" 因为我们放宽了限制,例如从水果到食物)。此外,通过在其 return 类型中实现协变,它需要 "same or more",这意味着它可以 return R 或任何比这更具体的东西(这里 "more" 意味着"subtype" 因为我们正在添加更多信息,例如从 Fruit 到 Apple)。

但是为什么呢?为什么不反过来呢?这是一个有望更直观地解释它的示例 - 想象两个具体函数,一个是另一个的子类型:

val f: Fruit => Fruit
val s: Food => Apple

函数 s 是函数 f 的有效子类型,因为它需要更少(我们 "lose" 从水果到食物的信息)并提供更多(我们 "gain" 从 Fruit 到 Apple 的信息)。请注意 s 的输入类型是 f 的输入类型的超类型(逆变),并且它的 return 类型是 f 的 return 类型(协方差)。现在让我们想象一段使用这些函数的代码:

def someMethod(fun: Fruit => Fruit) = // some implementation

someMethod(f)someMethod(s) 都是有效的调用。方法 someMethod 在内部使用 fun 对其应用水果,并从中接收水果。由于 sf 的子类型,这意味着我们可以提供 Food => Apple 作为 fun 的完美实例。 someMethod 中的代码会在某些时候用一些水果喂 fun,这没关系,因为 fun 需要食物,而水果 食物。另一方面,fun 具有 Apple 作为 return 类型也很好,因为 fun 应该 return 水果,并且通过 returning 苹果它遵守该合同。

我希望我能把它弄清楚一点,请随时提出更多问题。

TL;DR:

  • 您的 Pets class 可以通过 return 成员 生成 类型 A 的值变量 pet,所以一个 Pet[VeryGeneral] 不能是 Pet[VerySpecial] 的子类型,因为当它产生某些东西 VeryGeneral 时,它不能保证它也是 [=26= 的一个实例].因此不能逆变.

  • 您的 Pets class 可以 使用 类型 A 的值,方法是将它们作为参数传递给 add.因此 Pet[VerySpecial] 不能是宠物 Pet[VeryGeneral] 的子类型,因为它会阻塞任何非 VerySpecial 的输入。因此,你的class不能是协变的

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


###一个例子:协变与逆变:

我将利用这个机会展示一个改进的、更显着的 this comic 的严格版本。它是协方差协方差的说明 具有子类型和 declaration-site 方差注释的编程语言的概念 (显然,甚至 Java 人都觉得它很有启发性, 尽管问题是关于 use-site 方差)。

先上图:

现在用可编译的 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!")
}

现在,为什么 ConsumerA 中必须是逆变的?让我们尝试实例化 几种不同的能源,然后将它们提供给不同的消费者:

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 方法保证 return 只有非常特殊的 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]

产品之间的分型关系与产品之间的分型关系相同 产品的生产者。这就是协方差的意思。


相关链接

  1. Java 8 中关于 ? extends A? super B 的类似讨论:

  2. 经典“在我自己的 Either 实现中 flatMap 的正确类型参数是什么”问题: