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

时间:2018-02-15 16:43:50

标签: scala types functional-programming covariance contravariance

这是我阅读的有关scala中的逆变和协方差的文章之一的代码片段。但是,我无法理解scala编译器抛出的错误消息"错误:协变类型A出现在值pet2的类型A的逆变位置

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

我对这段代码片段的理解是,Pets是协变的并且接受A的子类型的对象。但是,函数add仅接受A类型的参数.Being covariant意味着Pets可以获取A类参数和它的亚型。那怎么会抛出错误呢。甚至出现了逆变问题。

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

2 个答案:

答案 0 :(得分:15)

<强> TL; DR:

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

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

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

插图:协方差与逆差:

我将借此机会展示一种改进的,更显着的 严谨的this comic版本。它是协方差逆变的说明 具有子类型和声明 - 站点方差注释的编程语言的概念 (显然,即便是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

结果是: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] ,即使之间的关系 类型参数恰恰相反:

class Entertainment
class Music extends Entertainment
class Metal extends Music // yes, it does, seriously^^

协方差的解释(图1的右侧部分)

定义产品层次结构:

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" }
}

定义各种&#34;来源&#34; /&#34;生产者&#34;不同专业水平:

BrowseYoutube

Entertainment是[{1}}最通用的来源:它可以给你 基本上任何类型的娱乐:猫视频,杂耍小丑,或(意外) 一些音乐。 这个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: Entertainment = BrowseYoutube.get   // No, probably not even music
// val metal2: Entertainment = RandomMusician.get  // No, could be Mozart, could be Rick Astley
val metal3: Entertainment = 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() static function

  2. 经典&#34;我flatMap实施中Either的正确类型参数是什么?#34;问题:Type L appears in contravariant position in Either[L, R]

答案 1 :(得分:3)

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

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

trait Function1[-T1, +R]

问题是,对于函数S来说,它是函数F的子类型,它需要(相同或更少)并提供(相同或更多)&#34; 。这也称为 Liskov替换原则。在实践中,这意味着功能特性需要在其输入中具有逆变性并且在其输出中需要协变。通过在输入中反向变换,它需要&#34;相同或更少&#34;,因为它接受T1或其任何超类型(这里&#34;更少&#34;意味着&#34;超类型& #34;因为我们正在放松限制,例如从水果到食物)。此外,通过在其返回类型中进行协变,它需要&#34;相同或更多&#34;,这意味着它可以返回R或更具体的内容(此处&#34;更多&#34;表示&#34;子类型&#34;因为我们正在添加更多信息,例如从Fruit到Apple)。

但为什么呢?为什么不相反呢?这里有一个例子,希望能更直观地解释它 - 想象两个具体的功能,一个是另一个的子类型:

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

函数s是函数f的有效子类型,因为它需要更少(我们&#34;丢失&#34;从水果到食物的信息)并提供更多(我们&#34)获得&#34;从水果到苹果的信息)。请注意s的输入类型是f的输入类型(逆变)的超类型,并且它的返回类型是{{1的子类型返回类型(协方差)。现在让我们设想一段使用这些函数的代码:

f

def someMethod(fun: Fruit => Fruit) = // some implementation someMethod(f)都是有效的调用。方法someMethod(s)在内部使用someMethod向其中应用水果,并从中接收水果。由于funs的子类型,这意味着我们可以提供f作为Food => Apple的完美优秀实例。 fun内的代码会在某个时刻向someMethod提供一些水果,这是正常的,因为fun需要食物,水果食物。另一方面,fun作为返回类型的fun也没问题,因为Apple应该返回水果,并且通过返回苹果它符合该合同。

我希望我能够澄清一点,随时提出进一步的问题。