Add enum construct
See original GitHub issueIntroduction
This is a proposal to add an enum
construct to Scala’s syntax. The construct is intended to serve at the same time as a native implementation of enumerations as found in other languages and as a more concise notation for ADTs and GADTs. The proposal affects the Scala definition and its compiler in the following ways:
- It adds new syntax, including a new keyword,
enum
. - It adds code to the scanner and parser to support the new syntax
- It adds new rules for desugaring enums.
- It adds a predefined trait
scala.Enum
and a predefined runtime classscala.runtime.EnumValues
.
This is all that’s needed. After desugaring, the resulting programs are expressible as normal Scala code.
Motivation
enum
s are essentially syntactic sugar. So one should ask whether they are necessary at all. Here are some issues that the proposal addresses:
-
Enumerations as a lightweight type with a finite number of user-defined elements are not very well supported in Scala. Using integers for this task is tedious and loses type safety. Using case objects is less efficient and gets verbose as the number of values grows. The existing library-based approach in the form of Scala’s
Eumeration
object has been criticized for being hard to use and for lack of interoperability with host-language enumerations. Alternative approaches, such as Enumeratum fix some of these issues, but have their own tradeoffs. -
The standard approach to model an ADT uses a
sealed
base class withfinal
case classes and objects as children. This works well, but is more verbose than specialized syntactic constructs. -
The standard approach keeps the children of ADTs as separate types. For instance,
Some(x)
has typeSome[T]
, notOption[T]
. This gives finer type distinctions but can also confuse type inference. Obtaining the standard ADT behavior is possible, but very tricky. Essentially, one has to make the case classabstract
and implement theapply
method in the companion object by hand. -
Generic programming techniques need to know all the children types of an ADT or a GADT. Furthermore, this information has to be present during type-elaboration, when symbols are first completed. There is currently no robust way to do so. Even if the parent type is sealed, its compilation unit has to be analyzed completely to know its children. Such an analysis can potentially introduce cyclic references or it is not guaranteed to be exhaustive. It seems to be impossible to avoid both problems at the same time.
I think all of these are valid criticisms. In my personal opinion, when taken alone, neither of these criticisms is strong enough to warrant introducing a new language feature. But taking them together could shift the balance.
Objectives
- The new feature should allow the concise expression of enumerations.
- Enumerations should be efficient, even if they define many values. In particular, we should avoid defining a new class for every value.
- It should be possible to model Java enumerations as Scala emumerations.
- The new feature should allow the concise expression of ADTs and GADTs.
- It should support all idioms that can be expressed with case classes. In particular, we want to support type and value parameters, arbitrary base traits, self types, and arbitrary statements in a case class and its companion object.
- It should lend itself to generic programming
Basic Idea
We define a new kind of enum
class. This is essentially a sealed
class whose instances are given by cases defined in its companion object. Cases can be simple or parameterized. Simple cases without any parameters map to values. Parameterized cases map to case classes. A shorthand form enum E { Cs }
defines both an enum class E
and a companion object with cases Cs
.
Examples
Here’s a simple enumeration
enum Color {
case Red
case Green
case Blue
}
or, even shorter:
enum Color { case Red, Green, Blue }
Here’s a simple ADT:
enum Option[T] {
case Some[T](x: T)
case None[T]()
}
Here’s Option
again, but expressed as a covariant GADT, where None
is a value that extends Option[Nothing]
.
enum Option[+T] {
case Some[T](x: T)
case None
}
It is also possible to add fields or methods to an enum class or its companion object, but in this case we need to split the `enum’ into a class and an object to make clear what goes where:
enum class Option[+T] extends Serializable {
def isDefined: Boolean
}
object Option {
def apply[T](x: T) = if (x != null) Some(x) else None
case Some[+T](x: T) {
def isDefined = true
}
case None {
def isDefined = false
}
}
The canonical Java “Planet” example (https://docs.oracle.com/javase/tutorial/java/javaOO/enum.html) can be expressed as follows:
enum class Planet(mass: Double, radius: Double) {
private final val G = 6.67300E-11
def surfaceGravity = G * mass / (radius * radius)
def surfaceWeight(otherMass: Double) = otherMass * surfaceGravity
}
object Planet {
case MERCURY extends Planet(3.303e+23, 2.4397e6)
case VENUS extends Planet(4.869e+24, 6.0518e6)
case EARTH extends Planet(5.976e+24, 6.37814e6)
case MARS extends Planet(6.421e+23, 3.3972e6)
case JUPITER extends Planet(1.9e+27, 7.1492e7)
case SATURN extends Planet(5.688e+26, 6.0268e7)
case URANUS extends Planet(8.686e+25, 2.5559e7)
case NEPTUNE extends Planet(1.024e+26, 2.4746e7)
def main(args: Array[String]) = {
val earthWeight = args(0).toDouble
val mass = earthWeight/EARTH.surfaceGravity
for (p <- enumValues)
println(s"Your weight on $p is ${p.surfaceWeight(mass)}")
}
}
Syntax Extensions
Changes to the syntax fall in two categories: enum classes and cases inside enums.
The changes are specified below as deltas with respect to the Scala syntax given here
-
Enum definitions and enum classes are defined as follows:
TmplDef ::= `enum' `class’ ClassDef | `enum' EnumDef EnumDef ::= id ClassConstr [`extends' [ConstrApps]] [nl] `{’ EnumCaseStat {semi EnumCaseStat} `}’
-
Cases of enums are defined as follows:
EnumCaseStat ::= {Annotation [nl]} {Modifier} EnumCase EnumCase ::= `case' (EnumClassDef | ObjectDef | ids) EnumClassDef ::= id [ClsTpeParamClause | ClsParamClause] ClsParamClauses TemplateOpt TemplateStat ::= ... | EnumCaseStat
Desugarings
Enum classes and cases expand via syntactic desugarings to code that can be expressed in existing Scala. First, some terminology and notational conventions:
-
We use
E
as a name of an enum class, andC
as a name of an enum case that appears in the companion object ofE
. -
We use
<...>
for syntactic constructs that in some circumstances might be empty. For instance<body>
represents either the body of a case between{...}
or nothing at all. -
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.
- Class cases are those cases that are parameterized, either with a type parameter section
Simple cases and value cases are called collectively singleton cases.
The desugaring rules imply that class cases are mapped to case classes, and singleton cases are mapped to val
definitions.
There are seven desugaring rules. Rules (1) and (2) desugar enums and enum classes. Rules (3) and (4) define extends clauses for cases that are missing them. Rules (4 - 6) define how such expanded cases map into case classes, case objects or vals. Finally, rule (7) expands comma separated simple cases into a sequence of cases.
-
An
enum
definitionenum E ... { <cases> }
expands to an enum class and a companion object
enum class E ... object E { <cases> }
-
An enum class definition
enum class E ... extends <parents> ...
expands to a
sealed
abstract
class that extends thescala.Enum
trait:sealed abstract class E ... extends <parents> with scala.Enum ...
-
If
E
is an enum class without type parameters, then a case in its companion object without an extends clausecase C <params> <body>
expands to
case C <params> <body> extends E
-
If
E
is an enum class with type parametersTs
, then a case in its companion object without an extends clausecase C <params> <body>
expands according to two alternatives, depending whether
C
has type parameters or not. IfC
has type parameters, they must have the same names and appear in the same order as the enum type parametersTs
(variances may be different, however). In this casecase C [Ts] <params> <body>
expands to
case C[Ts] <params> extends E[Ts] <body>
For the case where
C
does not have type parameters, assumeE
’s type parameters areV1 T1 > L1 <: U1 , ... , Vn Tn >: Ln <: Un (n > 0)
where each of the variances
Vi
is either'+'
or'-'
. Then the case expands tocase C <params> extends E[B1, ..., Bn] <body>
where
Bi
isLi
ifVi = '+'
andUi
ifVi = '-'
. It is an error ifBi
refers to some other type parameterTj (j = 0,..,n-1)
. It is also an error ifE
has type parameters that are non-variant. -
A class case
case C <params> ...
expands analogous to a case class:
final case class C <params> ...
However, unlike for a regular case class, the return type of the associated
apply
method is a fully parameterized type instance of the enum classE
itself instead ofC
. Also the enum case defines anenumTag
method of the formdef enumTag = n
where
n
is the ordinal number of the case in the companion object, starting from 0. -
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. -
A simple case
case C
of an enum class
E
that does not take type parameters expands toval C = $new(n, "C")
Here,
$new
is a private method that creates an instance of ofE
(see below). -
A simple case consisting of a comma-separated list of enum names
case C_1, ..., C_n
expands to
case C_1; ...; case C_n
Any modifiers or annotations on the original case extend to all expanded cases.
Enumerations
Non-generic enum classes E
that define one or more singleton cases are called enumerations. Companion objects of enumerations define the following additional members.
- A method
enumValue
of typescala.collection.immutable.Map[Int, E]
.enumValue(n)
returns the singleton case value with ordinal numbern
. - A method
enumValueNamed
of typescala.collection.immutable.Map[String, E]
.enumValueNamed(s)
returns the singleton case value whosetoString
representation iss
. - A method
enumValues
which returns anIterable[E]
of all singleton case values inE
, in the order of their definitions.
Companion objects that contain at least one simple case define in addition:
-
A private method
$new
which defines a new simple case value with given ordinal number and name. This method can be thought as being defined as follows.def $new(tag: Int, name: String): ET = new E { def enumTag = tag def toString = name $values.register(this) // register enum value so that `valueOf` and `values` can return it. }
Examples
The Color
enumeration
enum Color {
case Red, Green, Blue
}
expands to
sealed abstract class Color extends scala.Enum
object Color {
private val $values = new scala.runtime.EnumValues[Color]
def enumValue: Map[Int, Color] = $values.fromInt
def enumValueNamed: Map[String, Color] = $values.fromName
def enumValues: Iterable[Color] = $values.values
def $new(tag: Int, name: String): Color = new Color {
def enumTag: Int = tag
override def toString: String = name
$values.register(this)
}
final case val Red: Color = $new(0, "Red")
final case val Green: Color = $new(1, "Green")
final case val Blue: Color = $new(2, "Blue")
}
The Option
GADT
enum Option[+T] {
case Some[+T](x: T)
case None
}
expands to
sealed abstract class Option[+T] extends Enum
object Option {
final case class Some[+T](x: T) extends Option[T] {
def enumTag = 0
}
object Some {
def apply[T](x: T): Option[T] = new Some(x)
}
val None = new Option[Nothing] {
def enumTag = 1
override def toString = "None"
$values.register(this)
}
}
Note: We have added the apply
method of the case class expansion because
its return type differs from the one generated for normal case classes.
Implementation Status
An implementation of the proposal is in #1958.
Interoperability with Java Enums
On the Java platform, an enum class may extend java.lang.Enum
. In that case, the enum as a whole is implemented as a Java enum. The compiler will enforce the necessary restrictions on the enum to make such an implementation possible. The precise mapping scheme and associated restrictions remain to be defined.
Open Issue: Generic Programming
One advantage of the proposal is that it offers a reliable way to enumerate all cases of an enum class before any typechecking is done. This makes enums a good basis for generic programming. One could envisage compiler-generated hooks that map enums to their “shapes”, i.e. typelevel sums of products. An example of what could be done is elaborated in a test in the dotty repo.
Issue Analytics
- State:
- Created 7 years ago
- Reactions:124
- Comments:126 (89 by maintainers)
Top GitHub Comments
We have to disallow that.
This looks great!
I don’t think the long form is an improvement, though. The
case
keyword is all you need to disambiguate the members of the ADT from other stuff.I don’t see any issues here. I agree with Stefan that generics should be handled automatically by default, and have the type parameter missing and filled in as Nothing if the type is not referenced. If you want something else, you can do it explicitly.