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

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

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

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

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

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


TL; DR:

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

  • 您的Pets类可以通过将它们作为参数传递给add来使用类型为A的值。因此,Pet[VerySpecial]不能是pet Pet[VeryGeneral]的子类型,因为它会阻塞任何不是VerySpecial的输入。因此,你的班级不能协变。

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

    举例说明:协方差与逆差:

    我将利用这个机会提出一个改进的,显着的更多
    这部漫画的严谨版本。它是协方差和逆变的一个例证
    具有子类型和声明 - 站点方差注释的编程语言的概念
    (显然,即便是Java人员也发现它具有足够的启发性,
    尽管问题是关于使用场地差异的事实。

    首先,插图:

    covariance-contravariance-comic

    现在用可编译的Scala代码进行更详细的描述。

    对比差异的解释(图1的左侧部分)

    考虑以下能源层次结构,从非常一般到非常具体:

    1
    2
    3
    class EnergySource
    class Vegetables extends EnergySource
    class Bamboo extends Vegetables

    现在考虑具有单个consume(a: A)方法的特征Consumer[-A]

    1
    2
    3
    trait Consumer[-A] {
      def consume(a: A): Unit
    }

    让我们实现一些这个特性的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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中逆变?让我们尝试实例化
    一些不同的能源,然后将它们喂给各种消费者:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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[Vegetables]的地方可以替换Consumer[EnergySource]

    在需要Consumer[Bamboo]的情况下,可以替换Consumer[Vegetables]
    因此,Consumer[EnergySource] <: Consumer[Vegetables]Consumer[EnergySource] <: Consumer[Vegetables]是有道理的
    Consumer[Vegetables] <: Consumer[Bamboo],即使之间的关系
    类型参数完全相反:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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的右侧部分)

    定义产品层次结构:

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

    定义可以生成A类型值的特征:

    1
    2
    3
    trait Producer[+A] {
      def get: A
    }

    定义不同专业水平的各种"来源"/"生产者":

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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]。 A Producer[Metal]可以代替Producer[Music]
    一般来说,是生产者
    可以为不太专业的生产者提供更具体的产品:

    1
    2
    3
    4
    5
    6
    7
    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()静态函数

  • 经典"在我自己的Either实现中flatMap的正确类型参数是什么"问题:类型L出现在Either[L, R]中的逆变位置

  • 好。


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

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

    1
    trait Function1[-T1, +R]

    问题是,对于函数S是函数F的子类型,它需要"需要(相同或更少)并提供(相同或更多)"。这也称为Liskov替代原则。在实践中,这意味着功能特性需要在其输入中具有逆变性并且在其输出中需要协变。通过在其输入中逆变,它需要"相同或更少",因为它接受T1或其任何超类型(这里"较少"意味着"超类型",因为我们放松了限制,例如从水果到食物)。此外,通过在其返回类型中具有协变性,它需要"相同或更多",这意味着它可以返回R或更具体的东西(这里"更多"意味着"子类型",因为我们正在添加更多信息,例如来自水果到苹果)。

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

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

    函数S是函数F的有效子类型,因为它需要更少(我们"丢失"从Fruit到Food的信息)并提供更多(我们"获取"从Fruit到Apple的信息)。注意S的输入类型是F的输入类型(逆变)的超类型,它的返回类型是F的返回类型(协方差)的子类型。现在让我们想象一段使用这些函数的代码:

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

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

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