question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Add enum construct

See original GitHub issue

Introduction

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 class scala.runtime.EnumValues.

This is all that’s needed. After desugaring, the resulting programs are expressible as normal Scala code.

Motivation

enums are essentially syntactic sugar. So one should ask whether they are necessary at all. Here are some issues that the proposal addresses:

  1. 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.

  2. 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.

  3. 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.

  4. 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

  1. The new feature should allow the concise expression of enumerations.
  2. Enumerations should be efficient, even if they define many values. In particular, we should avoid defining a new class for every value.
  3. It should be possible to model Java enumerations as Scala emumerations.
  4. The new feature should allow the concise expression of ADTs and GADTs.
  5. 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.
  6. 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

  1. Enum definitions and enum classes are defined as follows:

    TmplDef ::=  `enum' `class’ ClassDef
             |   `enum' EnumDef
    EnumDef ::=  id ClassConstr [`extends' [ConstrApps]] 
                 [nl] `{’ EnumCaseStat {semi EnumCaseStat} `}’
    
  2. 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, 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:

    • 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.

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.

  1. An enum definition

     enum E ... { <cases> }
    

    expands to an enum class and a companion object

    enum class E ...
    object E { <cases> }
    
  2. 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 ...
    
  3. If E is an enum class without type parameters, then a case in its companion object without an extends clause

     case C <params> <body>
    

    expands to

     case C <params> <body> extends E
    
  4. If E is an enum class with type parameters Ts, then a case in its companion object without an extends clause

     case C <params> <body>
    

    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.

  5. 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 class E itself instead of C. Also the enum case defines an enumTag method of the form

     def enumTag = n
    

    where n is the ordinal number of the case in the companion object, starting from 0.

  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.

  7. A simple case

     case C
    

    of an enum class E that does not take type parameters expands to

     val C = $new(n, "C")
    

    Here, $new is a private method that creates an instance of of E (see below).

  8. 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 type scala.collection.immutable.Map[Int, E]. enumValue(n) returns the singleton case value with ordinal number n.
  • A method enumValueNamed of type scala.collection.immutable.Map[String, E]. enumValueNamed(s) returns the singleton case value whose toString representation is s.
  • A method 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.
       }
    

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:closed
  • Created 7 years ago
  • Reactions:124
  • Comments:126 (89 by maintainers)

github_iconTop GitHub Comments

11reactions
oderskycommented, Feb 13, 2017

Also, how would that interact with additional type parameters?

We have to disallow that.

8reactions
Ichorancommented, Feb 13, 2017

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.

enum Either[+L, +R] {
  def fold[Z](f: L => Z, g: R => Z): Z
  case Left(value: L) {
    def fold[Z](f: L => Z, g: Nothing => Z) = f(value)
  }
  case Right(value: R) {
    def fold[Z](f: Nothing => Z, g: R => Z) = g(value)
  }
}

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.

  case Right[+L, +R](value: R) extends Either[L, R]
Read more comments on GitHub >

github_iconTop Results From Across the Web

Java enum Constructor - Programiz
In this Java tutorial, you can learn about enum constructors with the help of a working example.
Read more >
Attaching Values to Java Enum - Baeldung
Let's start by adding the element names. ... First of all, we notice the special syntax in the declaration list. This is how...
Read more >
How do I use the enum type with a constructor? - avajava.com
A JavaSW enum type can have a private constructor that can be used to initialize instance fields. The EnumDemo class demonstrates this.
Read more >
Enum Types - Java™ Tutorials
Note: The constructor for an enum type must be package-private or private access. It automatically creates the constants that are defined at the...
Read more >
Java Examples - Use of Enum constructor, method
How to use enum constructor, instance variable & method? Solution. This example initializes enum using a costructor & getPrice() method & display values...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found