【问题标题】:type stable parametric polymorphism类型稳定参数多态性
【发布时间】:2021-07-11 23:08:51
【问题描述】:

我不明白为什么以下 scala 代码无法编译:

sealed trait A
case class B() extends A {
  def funcB: B = this
}
case class C() extends A {
  def funcC: C = this
}
def f[T <: A](s:T): T = s match {
  case s: B => s.funcB
  case s: C => s.funcC
}

可以将f替换为

def f[T <: A](s:T): A = s match {
  case s: B => s.funcB
  case s: C => s.funcC
}

然后在调用f 时转换为子类型,例如使用asInstanceOf。但是我希望能够构造一个函数来统一一些以前定义的方法,并让它们是类型稳定的。谁能解释一下?

另外,请注意以下f 也可以编译:

def f[T <: A](s:T): T = s match {
  case s: B => s
  case s: C => s
}

【问题讨论】:

  • 编译器无法知道s 将是A 类型。在某些情况下可行,但我不记得规则。通常,更简单的方法是改用typeclass。

标签: scala generics polymorphism type-parameter return-current-type


【解决方案1】:

什么使它起作用?

特别是在 Scala 3 中,您可以使用匹配类型

scala> type Foo[T <: A] = T match {
     |     case B => B
     |     case C => C
     | }
     |
     | def f[T <: A](s:T): Foo[T] = s match {
     |   case s: B => s.funcB
     |   case s: C => s.funcC
     | }
def f[T <: A](s: T): Foo[T]

scala> f(B())
val res0: B = B()

scala> f(C())
val res1: C = C()

一般来说,有关“返回当前类型”问题的解决方案,请参阅 Scala 常见问题解答How can a method in a superclass return a value of the “current” type?

诸如类型类和匹配类型之类的编译时技术可以被认为是一种编译时模式匹配,它指示编译器减少到调用站点使用的最具体的信息丰富的类型,而不是必须确定可能的较差的上限类型。

为什么它不起作用?

要理解的关键概念是参数多态性是一种通用量化,这意味着它必须对编译器对所有调用点类型参数的实例化有意义。考虑输入规范

def f[T <: A](s: T): T

编译器可能会这样解释

对于属于A 子类型的所有类型T,那么f 应该返回它 特定的子类型T。

因此表达式expr 代表f 的主体

def f[T <: A](s:T): T = expr

必须输入特定的T。现在让我们尝试输入我们的expr

s match {
  case s: B => s.funcB
  case s: C => s.funcC
}

类型

case s: B => s.funcB

是B,类型是

case s: C => s.funcC

是C。鉴于我们有B 和C,现在编译器必须取两者中最小的上限,即A。但是A 并不总是T。因此类型检查失败。

现在让我们做同样的练习

def f[T <: A](s: T): A

此规范的意思是(并再次遵守“为所有人”)

对于作为A 子类型的所有类型T,则f 应返回 他们的超类型A。

现在让我们输入方法体表达式

s match {
  case s: B => s.funcB
  case s: C => s.funcC
}

在我们到达类型B 和C 之前,因此编译器采用超类型A 的上限。事实上,这正是我们指定的返回类型。所以类型检查成功了。然而,尽管成功了,但在编译时我们丢失了一些输入信息,因为编译器将不再考虑在调用站点传入的特定 T 附带的所有信息,而只考虑通过其超类型 A 提供的信息。例如,如果T 有一个在A 中不存在的成员,那么我们将无法调用它。

要避免什么?

关于asInstanceOf,这是我们告诉编译器停止帮助我们,因为我们会冒雨。有两组人倾向于在 Scala 中使用它来使事情顺利进行,mad scientist 库的作者和从其他更动态类型的语言过渡的人。然而,在大多数应用程序级代码中,这被认为是不好的做法。

【讨论】:

    【解决方案2】:

    这一切都归结于我们的老朋友(恶魔?)编译时/运行时障碍。 (而且这对双胞胎永远不会相遇。)

    T 在调用站点的编译时解析。当编译器看到f(B) 时,T 表示B,当编译器看到f(C) 时,T 变为C。

    但是match { case ... 在运行时被解析。编译器无法知道将选择哪个case 分支。从编译器的角度来看,所有case 选项的可能性都相同。因此,如果T 解析为B,但代码可能采用C 分支...好吧,编译器不允许这样做。

    看看做了什么编译:

    def f[T <: A](s:T): A = s match { //f() returns an A
      case s: B => s.funcB            //B is an A sub-type
      case s: C => s.funcC            //C is an A sub-type
    }                                 //OK, all is good
    

    您的第二个“也有效”示例不适合我。

    【讨论】:

      【解决方案3】:

      回答问题为什么它不起作用。 f 返回语句 s match {...} 的结果。

      该语句的类型是A(有时它返回B,有时它返回C),不是T,因为它应该是。 T 有时是 C,有时是 B,s match {...} 从不其中任何一个。它是它们的超类型,即A。

      回复。这个:

       s match {
        case s: B => s
        case s: C => s
      }
      

      这条语句的类型显然是T,因为s是T。它肯定是does compile,尽管@jwvh 可能会说:)

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-05-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多