为什么协变枚举上的匹配与密封特征的匹配不同?

Why does a match on covariant enum does not behave the same as with a sealed trait?

为什么会产生错误?

import scala.io.StdIn

enum Flow[+A] {
  case Say (text: String) extends Flow[Unit]
  case Ask extends Flow[String]
}

def eval [T](flow: Flow[T]): T = flow match {
  case Flow.Say(text) => println(text)
  case Flow.Ask => StdIn.readLine // error here
}
18 |  case Flow.Ask => StdIn.readLine
   |                   ^^^^^^^^^^^^^^
   |                   Found:    String
   |                   Required: T

(如果枚举在 T 上保持不变则没有错误)

但是当枚举被替换为(看似等价?)具有密封特征的 ADT 时,不会报告此类错误:

sealed trait Flow[+A]
object Flow {
  case class Say(text: String) extends Flow[Unit]
  case object Ask extends Flow[String]
}
// match statement in eval works fine

我们可以查看 the initial proposal 向 Scala 3 添加枚举以获得一些见解。

根据该提案的措辞,枚举案例分为​​三类。

  • Class cases are those cases that are parameterized, either with a type parameter section [...] or with one or more (possibly empty) parameter sections (...).
  • Simple cases are cases of a non-generic enum class that have neither parameters nor an extends clause or body. That is, they consist of a name only.
  • Value cases are all cases that do not have a parameter section but that do have a (possibly generated) extends clause and/or a body.

让我们看看您的 enum 声明。

enum Flow[+A] {
  case Say (text: String) extends Flow[Unit]
  case Ask extends Flow[String]
}

现在,简单的案例就出来了,因为你的枚举是通用的。您的 Say 很明显是一个 class 案例,因为它是参数化的。另一方面,Ask 是非泛型和非枚举的,因此它是一个值案例。如果我们向下滚动一点,我们会看到值案例的作用

(6) A value case

case C extends <parents> <body>

expands to a value definition

val C = new <parents> { <body>; def enumTag = n; $values.register(this) }

where n is the ordinal number of the case in the companion object, starting from 0. The statement $values.register(this) registers the value as one of the enumValues of the enumeration (see below). $values is a compiler-defined private value in the companion object.

这里的底线是 Ask 没有定义为

object Ask extends Flow[String]

但更像

val Ask = new Flow[String]() { ... }

所以你的模式匹配

case Flow.Ask => StdIn.readLine

实际上是针对 Flow.Ask 相等性检查 ,而不是针对单例对象的类型检查。不幸的是,前者无法向编译器证明您的值的泛型类型,因此它仍然是 T,而不是专用于 String.

同一页上提供了这样做的理由

Objectives

...

  1. Enumerations should be efficient, even if they define many values. In particular, we should avoid defining a new class for every value.

...

因此,Scala 开发人员似乎希望避免枚举声明大量未参数化值(即看起来像普通 Java 样式枚举的类型)所导致的膨胀。

那么简单的解决方案就是提供一个参数列表

case Ask() extends Flow[String]

使用起来稍微麻烦一些,但它确实定义了一个新类型,然后你的模式匹配

case Flow.Ask() => StdIn.readLine

会成功