Adding Enumeratum to the Scala Platform

Thanks jvican,

I’d like to formally submit a proposal to have Enumeratum-core added to the SP.

#Introduction
Enumeratum is an enumerations library. Its goal is to make it easy to declare ADTs and use them as enumerations that can be deserialised from primitive values or Strings. Unlike Enumeration in the standard library, it is type safe.

Other highlights include:

  • Zero dependencies (Macros were written with no other dependencies like macro-paradise, or compatibility-shim libs) unless if you count scala-reflect.
  • Performant: Faster than Enumeration in the standard library (see benchmarks)
  • Idiomatic: you’re very clearly still writing Scala, and no funny colours in your IDE means less cognitive overhead for your team
  • Simplicity; most of the complexity in this lib is in its macro, and the macro is fairly simple conceptually
  • No usage of reflection at runtime. This may also help with performance but it means Enumeratum is compatible with ScalaJS and other environments where reflection is a best effort (such as Android)
  • Comprehensive automated testing
  • Integration with popular libraries like Play, Circe, ReactiveMongo, Argonaut, and UPickle

As for which parts to put into the SP, I think starting small makes it easier to keep the discussion going, so maybe the following core functionality would be nice:

  • The normal Enum[A <: EnumEntry]
  • The ValueEnums

The macros library will also need to be included because core depends on it.

#Example usage

Enum

scala> import enumeratum._
  
scala> sealed trait DummyEnum extends EnumEntry
  
scala> object DummyEnum extends Enum[DummyEnum] {
     |   val values = findValues
     |   case object Hello   extends DummyEnum
     |   case object GoodBye extends DummyEnum
     |   case object Hi      extends DummyEnum
     | }
  
scala> DummyEnum.withNameOption("Hello")
res0: Option[DummyEnum] = Some(Hello)
  
scala> DummyEnum.withNameOption("Nope")
res1: Option[DummyEnum] = None

ValueEnum

import enumeratum.values._

sealed abstract class LibraryItem(val value: Int, val name: String) extends IntEnumEntry

case object LibraryItem extends IntEnum[LibraryItem] {

  case object Book     extends LibraryItem(value = 1, name = "book")
  case object Movie    extends LibraryItem(name = "movie", value = 2)
  case object Magazine extends LibraryItem(3, "magazine")
  case object CD       extends LibraryItem(4, name = "cd")
  // case object Newspaper extends LibraryItem(4, name = "newspaper") <-- will fail to compile because the value 4 is shared

  /*
  val five = 5
  case object Article extends LibraryItem(five, name = "article") <-- will fail to compile because the value is not a literal
  */

  val values = findValues

}

assert(LibraryItem.withValue(1) == LibraryItem.Book)

LibraryItem.withValue(10) // => java.util.NoSuchElementException:

Design

As much as possible, Enum's API was written to look like Enumeration to facilitate smooth migration and adoption. Since then various contributors have added functionality such as nice string transforms for the names of the enum members (see mixins usage).

ValueEnum was written to solve the problem of not having compile-time uniqueness checks on Enum values and to be able to deserialise primitive values to Enum members.

Attention was also paid to small things like making sure the usage was natural in IntelliJ (no funny colours) and that the macros expanded into code that doesn’t bother WartRemover.

Implementation

Uses blackbox macros. ASTs are built manually w/o quasiquotes to kill off another dependency for 2.10 users.

Benchmarks

Details on the benchmarking setup are here.

Benchmarks for Scala version 2.11.8 on MBP 2013 running OSX El Capitan with JDK8:

[info] Benchmark                                            Mode  Cnt     Score    Error  Units
[info] EnumBenchmarks.indexOf                               avgt   30    11.628 ±  0.190  ns/op
[info] EnumBenchmarks.withNameDoesNotExist                  avgt   30  1809.194 ± 33.113  ns/op
[info] EnumBenchmarks.withNameExists                        avgt   30    13.540 ±  0.374  ns/op
[info] EnumBenchmarks.withNameOptionDoesNotExist            avgt   30     5.999 ±  0.037  ns/op
[info] EnumBenchmarks.withNameOptionExists                  avgt   30     9.662 ±  0.232  ns/op
[info] StdLibEnumBenchmarks.withNameDoesNotExist            avgt   30  1921.690 ± 78.898  ns/op
[info] StdLibEnumBenchmarks.withNameExists                  avgt   30    56.517 ±  1.161  ns/op
[info] values.ValueEnumBenchmarks.withValueDoesNotExist     avgt   30  1950.291 ± 29.292  ns/op
[info] values.ValueEnumBenchmarks.withValueExists           avgt   30     4.009 ±  0.062  ns/op
[info] values.ValueEnumBenchmarks.withValueOptDoesNotExist  avgt   30     5.285 ±  0.063  ns/op
[info] values.ValueEnumBenchmarks.withValueOptExists        avgt   30     6.621 ±  0.084  ns/op

Benchmark results for Scala version 2.11 on Linux 4.8.13-1-ARCH with Intel® Core™ i7-6600U CPU @ 2.60GHzm and Turbo enabled:

[info] Benchmark                                            Mode  Cnt     Score     Error  Units
[info] EnumBenchmarks.indexOf                               avgt   30    11.211 ±   0.805  ns/op
[info] EnumBenchmarks.withNameDoesNotExist                  avgt   30  1369.675 ±  97.946  ns/op
[info] EnumBenchmarks.withNameExists                        avgt   30    11.727 ±   0.473  ns/op
[info] EnumBenchmarks.withNameOptionDoesNotExist            avgt   30     5.148 ±   0.345  ns/op
[info] EnumBenchmarks.withNameOptionExists                  avgt   30     8.035 ±   0.228  ns/op
[info] StdLibEnumBenchmarks.withNameDoesNotExist            avgt   30  1538.766 ± 163.269  ns/op
[info] StdLibEnumBenchmarks.withNameExists                  avgt   30    54.505 ±   1.419  ns/op
[info] values.ValueEnumBenchmarks.withValueDoesNotExist     avgt   30  1473.439 ±  78.215  ns/op
[info] values.ValueEnumBenchmarks.withValueExists           avgt   30     3.175 ±   0.111  ns/op
[info] values.ValueEnumBenchmarks.withValueOptDoesNotExist  avgt   30     4.749 ±   0.159  ns/op
[info] values.ValueEnumBenchmarks.withValueOptExists        avgt   30     5.639 ±   0.266  ns/op

And with Turbo disabled:

[info] Benchmark                                            Mode  Cnt     Score    Error  Units
[info] EnumBenchmarks.indexOf                               avgt   30    13.080 ±  0.098  ns/op
[info] EnumBenchmarks.withNameDoesNotExist                  avgt   30  1672.842 ± 81.636  ns/op
[info] EnumBenchmarks.withNameExists                        avgt   30    17.010 ±  2.512  ns/op
[info] EnumBenchmarks.withNameOptionDoesNotExist            avgt   30     6.561 ±  0.149  ns/op
[info] EnumBenchmarks.withNameOptionExists                  avgt   30    10.070 ±  0.065  ns/op
[info] StdLibEnumBenchmarks.withNameDoesNotExist            avgt   30  1841.427 ± 42.465  ns/op
[info] StdLibEnumBenchmarks.withNameExists                  avgt   30    67.414 ±  2.356  ns/op
[info] values.ValueEnumBenchmarks.withValueDoesNotExist     avgt   30  1713.638 ± 91.957  ns/op
[info] values.ValueEnumBenchmarks.withValueExists           avgt   30     4.136 ±  0.183  ns/op
[info] values.ValueEnumBenchmarks.withValueOptDoesNotExist  avgt   30     6.144 ±  0.215  ns/op
[info] values.ValueEnumBenchmarks.withValueOptExists        avgt   30     6.766 ±  0.174  ns/op
5 Likes