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:
enum
.scala.Enum
and a predefined runtime class scala.runtime.EnumValues
.This is all that's needed. After desugaring, the resulting programs are expressible as normal Scala code.
Motivationenum
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 with final
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 type Some[T]
, not Option[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 class abstract
and implement the apply
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.
ObjectivesWe 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
.
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
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, and C
as a name of an enum case that appears in the companion object of E
.
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:
[...]
or with one or more (possibly empty) parameter sections (...)
.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
definition
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 the scala.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 clause
expands to
case C <params> <body> extends E
If E
is an enum class with type parameters Ts
, then a case in its companion object without an extends clause
expands according to two alternatives, depending whether C
has type parameters or not. If C
has type parameters, they must have the same names and appear in the same order as the enum type parameters Ts
(variances may be different, however). In this case
case C [Ts] <params> <body>
expands to
case C[Ts] <params> extends E[Ts] <body>
For the case where C
does not have type parameters, assume E
's type parameters are
V1 T1 > L1 <: U1 , ... , Vn Tn >: Ln <: Un (n > 0)
where each of the variances Vi
is either '+'
or '-'
. Then the case expands to
case C <params> extends E[B1, ..., Bn] <body>
where Bi
is Li
if Vi = '+'
and Ui
if Vi = '-'
. It is an error if Bi
refers to some other type parameter Tj (j = 0,..,n-1)
. It is also an error if E
has type parameters that are non-variant.
A class case
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 class E
itself instead of C
. Also the enum case defines an enumTag
method of the form
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 the enumValues
of the
enumeration (see below). $values
is a compiler-defined private value in
the companion object.
A simple case
of an enum class E
that does not take type parameters expands to
Here, $new
is a private method that creates an instance of of E
(see below).
A simple case consisting of a comma-separated list of enum names
expands to
Any modifiers or annotations on the original case extend to all expanded cases.
Non-generic enum classes E
that define one or more singleton cases are called enumerations. Companion objects of enumerations define the following additional members.
enumValue
of type scala.collection.immutable.Map[Int, E]
. enumValue(n)
returns the singleton case value with ordinal number n
.enumValueNamed
of type scala.collection.immutable.Map[String, E]
. enumValueNamed(s)
returns the singleton case value whose toString
representation is s
.enumValues
which returns an Iterable[E]
of all singleton case values in E
, 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.
}
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.
An implementation of the proposal is in #1958.
Interoperability with Java EnumsOn 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.
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.
rgladwell, lare96, oskoi, ferd36, vmorarian and 35 moreikempfliufengyun, rinfield, felixmulder, ktoso, Elyrixia and 78 more
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4