首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >在函数参数A的逆变位置接受协变类型A => B

在函数参数A的逆变位置接受协变类型A => B
EN

Stack Overflow用户
提问于 2021-07-03 17:17:01
回答 2查看 216关注 0票数 3

考虑协变类型参数A

代码语言:javascript
复制
case class Foo[+A](a: A):
  def bar(a: A) = a             // error: covariant type A occurs in contravariant position 
  def zar(f: A => Int) = f(a)   // ok
             |
          This is contravariant position. Why is it ok?

Foo(41).zar(_ + 1) // : Int = 42

为什么当它出现在A => Int中的逆变量位置时,它会被接受为zar的参数

EN

回答 2

Stack Overflow用户

发布于 2021-07-03 20:15:34

遵循@sarveshseri的想法,我将提供一个直观的解释,而不是一个正式的解释。这既是因为我不知道正式版本的细节,也是因为我希望这一版本对读者更有帮助。

首先是一些免责声明:

我可能有打字错误或一些错误,如果你注意到了,请编辑答案,如上所述,我将描述的心理模型是一个近似值,在编译时和运行时实际发生的情况将是different.

  • During
  1. ,我将指的是类型和类可互换。这是错误的,和我自己已经在其他答案上指出了这一点。在这种情况下,这是为了简化这个心理模型;但我建议在方差单击之后,将类型和类的区别混合到该模型中:https://typelevel.org/blog/2017/02/13/more-types-than-classes.html
  2. Connected到前面的点,我将在我的示例中使用具体/简单类型。谢天谢地,由于类型擦除和参数化,我将解释的适用于类型构造函数和其他复杂类型。我还将交替使用方法和函数,this is

现在让我们开始吧。首先让我们假设,如果一个方法接受一个期间,那么它只能接受Foo类型的值,而不能接受其他类型的值,Foo

然后让我们假设子类型实际上是一个“隐式”函数,它“强制转换”值。所以B <: A就是B => A

让我们假设这样的“强制转换”是一种伪装,值实际上是相同的,但被不同地看待(这基本上是Liskov原则)。

因此,当您尝试将B传递给需要A的方法时,编译器将插入此隐式强制转换。这样,在运行时,该方法会接收一个类似于A类型的值,而不是B类型的值;但该值仍然是B类型(实际上我们在这里讨论的是类而不是types__,但我希望您能理解)。

现在,让我们看看协变类Foobarbaz方法发生了什么

由于Foo[Dog] <: Foo[Animal] I可以将前者转换为后者,那么在给定Cat <: Animal的情况下,我也可以将前者转换为后者。最后,我可以将这个伪装成AnimalCat传递给伪装成Foo[Animal]Foo[Dog]bar方法,但是在运行时,我们会将一个Cat传递给一个需要Dog kataplum的方法!当然,除非这种方法总是为这种情况做好了准备。

这就是为什么必须像[B >: A](b: B)一样定义bar。这里我们说的是,我们可以接受编译器可以为其生成隐式强制转换函数A => B的任何B (与前面相反,多亏了Any,这样的类型和函数总是可能的)。然后,bar的实现应该能够处理这个新类型的B,并在需要时使用cast函数。

这意味着前面的示例不会在运行时崩溃,因为我们可以直接传递Cat,而不需要通过间接伪装;这之所以有效,是因为编译器总是能够推断B应该是Animal ( CatDog__的LUB ),所以它会将Cat转换为Animal,并将其传递给bar和强制转换函数Dog => Animal

请注意,A => B cast函数的存在意味着,如果F[A] => F[B]是协变的,编译器也可以创建F函数。

现在让我们看看baz发生了什么。同样,我们会将一个Foo[Dog]转换为一个Foo[Animal],然后我们会尝试使用一个应该在运行时工作的函数Animal => Int来调用baz,因为我们甚至根本不需要伪装,我们可以直接将这样的函数传递给Foo[Dog],因为(Animal => Int) <: (Dog => Int)这是因为函数在其输入上是逆变量。

但这到底是如何工作的呢?简单地说,直觉告诉我,如果我能够处理/消费/接收/使用任何Animal,那么我应该能够处理任何Dog,因为它们是Animals,对吧?让我们看看这是如何与我们的心理模型一起工作的。

我有baz(Dog => Int)f(Animal => Int),编译器能做的就是创建一个新函数g(Dog => Int) = cast(Dog => Animal) andThen f(Animal => Int)并使用g

希望这能有所帮助,请随时留下任何问题。

票数 6
EN

Stack Overflow用户

发布于 2021-07-03 21:20:56

您的类可以被视为

代码语言:javascript
复制
trait Function1[-Input, +Result]  // Renamed the type parameters for clarity

case class Foo[+A](a: A) {
  val bar: Function1[A, A] = identity
  val zar: Function1[Function1[A, Int], Int] = { f => f(a) }
}

编译器采用的方法是在签名中的每个类型位置分配正、负和中性注释;我将通过在该位置的类型后面添加+和-来标记这些位置

首先,顶级val是正的(即协变):

代码语言:javascript
复制
case class Foo[+A](a: A) {
  val bar: Function1[A, A]+
  val zar: Function1[Function1[A, Int], Int]+
}

bar可以是Function1[A, A]的子类型,zar可以是Function1[Function1[A, Int], Int]的子类型,所以这是有意义的(LSP等)。

然后,编译器进入类型参数。

代码语言:javascript
复制
case class Foo[+A](a: A) {
  val bar: Function1[A-, A+]
  val zar: Function1[Function1[A, Int]-, Int+]
}

由于Input是逆变量,这将相对于其周围的分类“翻转”分类(+ -> -,- -> +,中性不变)。Result协变不会翻转分类(如果Function1中有不变参数,则会强制分类为中性)。第二次应用此方法

代码语言:javascript
复制
case class Foo[+A](a: A) {
  val bar: Function1[A-, A+]
  val zar: Function1[Function1[A+, Int-], Int+]
}

要定义的类的类型参数只能在协变时使用+ position,在逆变时使用- position,如果不变则在任何地方使用(Int,它不是类型参数,在此分析中可以认为它是不变的:即,一旦我们得到既不是类型参数也没有类型参数的类型,我们就可以省去注释)。在bar中,我们有一个冲突( A-),但在zar中,A+意味着没有冲突。

Luis的回答中提出的生产/消费关系是一个很好的直观总结。这是一个部分(带有类型参数的方法使这一点变得复杂,尽管我不完全确定我的转换在那里是如何工作的…)探索编译器如何实际得出zar中的A处于协变位置的结论;在Scala (Odersky,Spoon,Venners)中的编程更详细地描述了这一点(在第3版中,它在19.4节中)。

票数 4
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/68234835

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档