如何检查函数中元素的协变和逆变位置?
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
对其应用水果,并从中接收水果。由于 s
是 f
的子类型,这意味着我们可以提供 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!")
}
现在,为什么 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" }
}
BrowseYoutube
是 Entertainment
最通用的来源:它可以给你
基本上 任何 种娱乐:猫视频、杂耍小丑,或(不小心)
一些音乐。
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]
产品之间的分型关系与产品之间的分型关系相同
产品的生产者。这就是协方差的意思。
相关链接
Java 8 中关于 ? extends A
和 ? super B
的类似讨论:
经典“在我自己的 Either
实现中 flatMap
的正确类型参数是什么”问题:
这是我阅读的一篇关于 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
对其应用水果,并从中接收水果。由于 s
是 f
的子类型,这意味着我们可以提供 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!")
}
现在,为什么 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" }
}
BrowseYoutube
是 Entertainment
最通用的来源:它可以给你
基本上 任何 种娱乐:猫视频、杂耍小丑,或(不小心)
一些音乐。
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]
产品之间的分型关系与产品之间的分型关系相同 产品的生产者。这就是协方差的意思。
相关链接
Java 8 中关于
? extends A
和? super B
的类似讨论:经典“在我自己的
Either
实现中flatMap
的正确类型参数是什么”问题: