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

12
这是我在查阅有关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的参数。协变意味着Pets可以接受类型为A及其子类型的参数。那么这个代码应该怎么出错呢?反变甚至不应该出现任何问题。

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

2个回答

44

TL;DR:

  • 你的 Pets 类可以通过返回成员变量 pet 来产生类型为 A 的值,因此 Pet[VeryGeneral] 不能是 Pet[VerySpecial] 的子类型,因为当它产生一个 VeryGeneral 的东西时,它不能保证它也是一个 VerySpecial 的实例。因此,它不能是逆变的

  • 你的 Pets 类可以通过将类型为 A 的值作为参数传递给 add 来消耗这些值。因此,Pet[VerySpecial] 不能是 Pet[VeryGeneral] 的子类型,因为它会在任何不是 VerySpecial 的输入上出错。因此,你的类不能是协变的

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


###说明:协变与逆变

我将利用这个机会呈现一个改进且更加严谨的版本这个漫画。它是一种编程语言的协变和逆变概念的说明,具有子类型和声明站点的方差注释(显然,即使Java人也发现它足够启迪人心,尽管问题是关于使用站点方差的)。

首先,这是插图:

covariance-contravariance-comic

现在是更详细的说明,包括可编译的Scala代码。

###逆变解释(图1左侧)

考虑以下能源来源层次结构,从非常通用到非常特定:

class EnergySource
class Vegetables extends EnergySource
class Bamboo extends Vegetables

现在考虑一个特性Consumer[-A],它只有一个consume(a: 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

结果是: 可以消耗广义食草动物所能消耗的一切,反过来广义食草动物可以消耗熊猫所能吃的一切。因此,只要我们关心能量来源的消耗能力,就可以在需要蔬菜消费者的地方替换为能量源消费者,并且可以在需要竹子消费者的地方替换为蔬菜消费者。因此,能量源消费者 <: 蔬菜消费者蔬菜消费者 <: 竹子消费者是有意义的,即使类型参数之间的关系完全相反:
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是最通用的娱乐来源之一:它可以提供基本上任何类型的娱乐:猫视频、杂耍小丑或(偶然)一些音乐。在图1中,这种通用的娱乐来源由原型小丑代表。

RandomMusician已经有了一定的专业性,至少我们知道这个对象产生音乐(即使没有限制于特定的流派)。

最后,MetalBandMember非常专业化:保证get方法仅返回非常具体的金属音乐。

让我们尝试从这三个对象获取不同类型的娱乐

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的类似讨论:Java 8 Comparator comparing() 静态函数

  2. 经典问题:“在我自己的Either实现中,flatMap的正确类型参数是什么”:Type L appears in contravariant position in Either[L, R]


阅读您的示例后,我确实清楚明了,因为网络上有成千上万的类似苹果香蕉示例的博客。唯一我不理解的是:为什么您选择以下逆变示例trait Consumer[-A] { def consume(a: A): Unit }和下面协变示例呢?trait Producer[+A] { def get: A } - PainPoints

4

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

例如,接受一个参数的函数具有以下定义:

trait Function1[-T1, +R]

事实上,对于一个函数S成为函数F的子类型来说,它需要“要求(相同或)更少并提供(相同或)更多”。这也被称为Liskov替换原则。在实践中,这意味着Function特质需要在其输入方面是逆变的,在其输出方面是协变的。通过在其输入方面进行逆变,它需要“相同或更少”,因为它接受T1或任何其超类型(这里的“更少”意味着“超类型”,因为我们放宽了限制,例如从Fruit到Food)。此外,通过在其返回类型方面进行协变,它需要“相同或更多”,这意味着它可以返回R或任何比它更具体的东西(这里的“更多”意味着“子类型”,因为我们添加了更多信息,例如从Fruit到Apple)。
但为什么呢?为什么不反过来呢?下面是一个例子,希望能更直观地解释 - 想象两个具体的函数,其中一个是另一个的子类型:
val f: Fruit => Fruit
val s: Food => Apple

函数sf函数的有效子类型,因为它需要更少的信息(从水果到食品时我们“失去”了信息),并提供更多的信息(从水果到苹果时我们“获得”了信息)。请注意,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" 应该如何修改以消除编译错误? - Chaitanya
1
@ChaitanyaWaikar 取决于你想做什么... 我总是更愿意远离方差并定义 class Pets[A],但如果这不是一个选项,你可以通过 def add[B >: A](pet2: B): String = "done" 来规避它。 - slouc

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接