Why does this produce an error?
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
(no error if the enum is left invariant on T)
But when the enum is replaced with a (seemingly equivalent?) ADT implemented with a sealed trait, no such error is reported:
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
We can look at the initial proposal which added enums to Scala 3 for some insight.
By the wording of this proposal, enum cases fall into three categories.
- 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.
Let's take a look at your enum
declaration.
enum Flow[+A] {
case Say (text: String) extends Flow[Unit]
case Ask extends Flow[String]
}
Now, simple cases are right out, because your enum is generic. Your Say
is pretty clearly a class case, since it's parameterized. Ask
, on the other hand, is non-generic and non-enumerated, so it's a value case. If we scroll down a bit, we see what value cases do
(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 theenumValues
of the enumeration (see below).$values
is a compiler-defined private value in the companion object.
The bottom line here is that Ask
is not defined as
object Ask extends Flow[String]
but more like
val Ask = new Flow[String]() { ... }
So your pattern match
case Flow.Ask => StdIn.readLine
is actually an equality check against the value Flow.Ask
, rather than a type check against a singleton object. The former, unfortunately, proves nothing to the compiler about the generic type of your value, so it remains T
, as opposed to specializing to String
.
The rationale for this is provided on the same page
Objectives
...
- Enumerations should be efficient, even if they define many values. In particular, we should avoid defining a new class for every value.
...
So it seems the Scala developers wanted to avoid the bloat that would result from an enum declaring tons of unparameterized values (i.e. a type that looks like an ordinary Java-style enum).
The simple solution, then, is to provide a parameter list
case Ask() extends Flow[String]
It's ever so slightly more cumbersome to work with, but it does define a new type, and then your pattern match
case Flow.Ask() => StdIn.readLine
will succeed