Adding Enumeratum to the Scala Platform


#1

@mdedetrich suggested that it might be a good idea to try to get Enumeratum, an enums library, into the Scala Platform.

I understand that there have been some ideas and progress made by the Scala team on fixing/improving enums, including a PR a while back (that may have turned into an SIP?) as well as union types in Dotty, so I would like to garner some feedback as well as advice on how to get started.


First Scala Platform meeting
#2

Hey @lloydmeta!

This is a great idea, I would very much like to see Enumeratum in the SP. :grinning:

To the best of my knowledge, the effort of improving enumerations is not active anymore. This is not relevant for the inclusion in the SP though, unless you’re eager to finish the work on that front and submit a SIP. This work is unrelated to the Scala Platform which only deals with Scala libraries and not with changes in the compiler.

The Scala Platform process goes as follows: you submit a proposal to include the library into the Scala Platform. In this proposal, you may want to touch on the following topics: goal and motivation of your library, most important features, which parts you want to include, other software alternatives (if they exist) and licensing. You can get some inspiration from this template. This proposal is then reviewed by the Scala Platform Committee and it will be voted on (we’re having our first meeting on the 17th January at 17:00 GMT+1). When the proposal is accepted, the library is immediately incubated with the help of other Scala developers and the process begins.

All the submission steps are defined here. Information on the incubation process is here. As I have commented in the original issue that proposed the inclusion, I’m happy to make a PR integrating sbt-platform and the Drone integration into Enumeratum.

All in all, these are the next steps:

  1. Create a proposal and submit it in this thread.
  2. Recruit Scala developers that are happy to contribute to Enumeratum and help you maintain the project.

Having some insights on the current rules of the process would be very helpful. So, feel free to chime in in the ongoing discussions of the Platform and its rules. On a side note, I will involve myself in the first proposals for the Scala Platform and help you get started, integrate with the tooling and find maintainers. Invoke me with @jvican!


#3

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

Adding better-files to the Scala Platform
#4

Thanks to you, we’re discussing this proposal on the 17th January! I’m creating a thread to keep track of the meeting agenda soon.

On a note, the inclusion of this library would mean the whitelisting of scala-reflect in the Platform, which has already been mostly agreed on this Discourse thread and I think it’s the way to go.

I’m forwarding this to all the Committee members so that they can have a look at this proposal.


#5

By the way, I’ve just updated your proposal to inline the benchmark results in your readme and add my results in my Linux machine. Hope that’s useful for other folks!


#6

Hi,

First, note that I’m the author of a similar project, so I’m probably biaised.

I think that an enumeration library would be a great addition to the platform!

Enumeratum looks great and has lots of features, but I think some of these features add too much complexity (for instance, the mixins that override the naming convention). I think libraries included in the platform should be more focused on just one problem. In my opinion, the “name” of an enumeration value should be an implementation detail and the naming convention used (camel case, sneak case, etc.) should not affect people usage. If you want to associate custom names to your enumeration values, why not just use a function Value => String (from which you can also derive an inverse partial function String => Option[Value])?

Also, I’d rather make withName and withValue explicit partial functions (ie. functions returning Option[Value]).

Finally, I have a question about your design: would it be possible to define an enumeration in a project and then to get a QueryStringBindable instance for this enumeration, in another project that uses Play (but without modifying the project that defines the enumeration)? I think that’s a very important use case because it would make it possible for enumeration authors to publish them and for enumeration users to retroactively add new capabilities to them. Last, I think it is super important to be able to use different representations of the enumeration values in (for instance) QueryStringBindable and BSONWriter because you sometime want to have control on the names used in a query string or to have control of the BSON representation for efficiency reasons.

One more question actually: is it possible to have a polymorphic enumeration type? E.g.:

sealed trait Foo[A]
case object Bar extends Foo[Int]
case object Baz extends Foo[String]

#7

(Note that I’m not familiar with your library, so I may be missing some nuances of your viewpoint.)

While I can understand that view, I have to come down on Enumeratum’s side on this point. It’s an implementation detail if you’re only doing pure Scala. But if you’re doing systems integration, working with other systems that have their own strong opinions about naming conventions, this ability to auto-translate the names looks to be a godsend. (I’ve only picked up Enumeratum this month, so I haven’t used this feature myself yet, but I’ve wanted it many times in the past.) It’s a great boilerplate-reducer.

That said, I can see the argument for separating the name-translation from the identity of the Enumeration itself, to be able to do multiple distinct translations for a given value. (Your QueryStringBindable vs BSONWriter case.) But the completely general Value => String just restates the usual boilerplate – the whole benefit here is automatic translation between the name in code and the external representation, same as is done in many serialization libraries. I wouldn’t want something that generalized, but I could see wanting to be able to define the translation as a subclass or typeclass rather than mixed into the enumeration itself, if that was possible, so that a given enumeration could be expressed differently for different interfaces…


First Scala Platform meeting
#8

Enumeratum has a lot of features, good performance and would be a great addition!

One thing I dislike is the dependency chain you introduce. For example if you have domain project which only contains plain Scala code with no database or web framework dependencies, it’s not nice to add enumeratum-play module which pulls in all the stuff from Play.

I’ve switched to @julienrf enum’s lib for that matter.

Would it be possible to use type classes in enumeratum to eliminate this issue? :slight_smile:


#9

These components will not be added to the Scala Platform, only core will. So it will be dependency-free! :smile:. Details here.
EDIT: Sorry, I misunderstood what you said. I think that exploring a typeclass approach would be interesting. What do you think @lloydmeta?


#10

Thanks all for the feedback and questions. I’ll try to get back to some of the comments and answer some questions here.

Before that, I’d like to mention again that Enumeratum strives to be easy to migrate to from the standard lib’s Enumeration, and be easy to use and understood. This is why the main way of using it revolves around using inheritance, and why out of the box, core contains mixins for manipulating enum entry names (a kind contribution from users who are actively using Enumeratum in industry and were tired of boilerplate).

As a result, I don’t see the following happening any time soon, especially because there are already partial function versions of them available side by side that users can choose to use if they wish.

Also, I’d rather make withName and withValue explicit partial functions (ie. functions returning Option[Value]).

That said, there are other ways to use enumeratum that do not involve inheritance and mixing in framework functionality into the enum. I hope this answers @julienrf and @Fristi 's questions (boils down to “would it be possible to define an enumeration in a project and then to get a QueryStringBindable instance for this enumeration, in another project that uses Play (but without modifying the project that defines the enumeration)?” ).

First, note that an Enum is type parameterised by its EnumEntry, so there is a materializeEnum implicit def on Enum (again, another awesome contribution from a user), enabling you to do stuff like this:

def findEnum[A <: EnumEntry: Enum](v: A) = implicitly[Enum[A]]

I’ll leave it to your imagination what you can do with something like that.

Now, for integration libraries, all of them revolve around building instances for typeclasses declared in frameworks anyway. And while it is true that the readme only introduces how to use them using inheritance, everything that is built via inheritance is actually delegated to plain-jane functions. So here goes a demonstration (I hope ammonite is ok):

Loading...
Welcome to the Ammonite Repl 0.8.1
(Scala 2.12.1 Java 1.8.0_101)
@ import $ivy.`com.beachape::enumeratum:1.5.6`, enumeratum._ 
import $ivy.$                                     , enumeratum._
@  // Declare your framework-free enum which you can publish with just a single dependency on Enumeratum
@ {
  sealed trait Greeting extends EnumEntry
  
  object Greeting extends Enum[Greeting] {
  
    val values = findValues
  
    case object Hello   extends Greeting
    case object GoodBye extends Greeting
    case object Hi      extends Greeting
    case object Bye     extends Greeting
  
  }
  } 
defined trait Greeting
defined object Greeting
@ // Then in your other projects where you use a lib like Circe, or a framework like Play, just instantiate typeclass instances-adhoc
@ import $ivy.`com.beachape::enumeratum-circe:1.5.6`, enumeratum.Circe, io.circe.syntax._ 
import $ivy.$                                     , enumeratum.Circe, io.circe.syntax._
@  
@ implicit val encoder = Circe.encoder(Greeting) 
encoder: io.circe.Encoder[Greeting] = enumeratum.Circe$$anon$1@2bcaec2e
@ Greeting.values.foreach  { v =>
    println(v.asJson)
  } 
"Hello"
"GoodBye"
"Hi"
"Bye"

That was for Circe, for Play there is this function which builds a typeclass instance of a Play Json Format for you. The other integration libraries have the same style. Note that we are now talking more so about how the optional integration libraries work with enumeratum-core…

One more question actually: is it possible to have a polymorphic enumeration type?

No, not at the moment. That said I’m not opposed to the idea of doing something like that if it can be implemented well.


#11

Thanks a lot for your clear answer!


#12

I don’t consider this to be an advantage. Ideally, we should be reusing code across the broader ecosystem. In particular, is there a reason why this is not built using shapeless (the Generic type class comes to mind), instead of implementing a special-purpose macro?


#13

I don’t consider this to be an advantage. Ideally, we should be reusing code across the broader ecosystem.

I can see where you’re coming from. Yes, that is a nice ideal, but is not without its drawbacks in practice.

Not having dependencies has the following advantages (to name a few):

  1. Lower barrier to adoption (2.10.x users don’t need to add another line of code for the paradise plugin)
  2. Reduces chances of version-conflicts / dependency-hell for end users (and their users …)
  3. No need to keep this lib superficially up-to-date/compatible with libraries it depends on (for the sake of 2)
  4. End users don’t need to download and package yet another dependency they don’t need or want (e.g. FiloDB uses enumeratum and has an explicit exclude rule on Shapeless)
  5. End users’ namespaces don’t get polluted by another dependency they don’t need

Of course, the disadvantage is that I need to maintain the macro. However, it’s a fairly simple macro that 1. is heavily tested and 2. hasn’t really needed much maintenance. In the last 2 years of so, asides from formatting and minor refactoring, I’ve barely touched it.

is there a reason why this is not built using shapeless (the Generic type class comes to mind), instead of implementing a special-purpose macro?

To name some:

  1. There are advantages to not having dependencies (see above)
  2. This lib solves a very small (yet fundamental) problem; pulling in Shapeless felt like overkill.
  3. Having our own macro means we have better control over compile time errors and can provide nicer and more relevant messages for users.
  4. When this lib was written over 2 years ago, SI-7046 was still a basket case. I know there have been improvements, but I’m still not sure if it’s safe enough. An issue filed (in Enumeratum no less…) < a month ago because a user was accidentally using Circe-generic (which was using knownDirectSubclasses via Shapeless) seems to indicate otherwise. This user was on Scala 2.12.1.
  5. Enumeratum guarantees that findValues will return an IndexedSeq of your enum members in declaration order. This allows for intuitive ordinals for enum members via indexOf. Anything that relies on knownDirectSubclasses is out of the running because that method returns a Set.
  6. The compile-time unique values checking done in ValueEnum is special purpose enough to prevent this migration all by itself.

Having written this, I realise it may look like I’m anti-Shapeless. Far from it: I’ve used shapeless a lot myself, but I think in the context of Enumeratum, there are advantages to not using it.


#14

Re-iterating that zero dependencies is actually a very powerful benefit for libraries in the SP, especially for ones that offer critical functionality (i.e. enums)


#15

I’m not entirely convinced enums are critical functionality.

Empirically, Scala survived for years without them, and seems to be doing alright, so it can’t be that critical.

Watching the SPP discussion, a lot of the discussion came down to how much Enumeratum brings to the table. What Scala provides right now in comparable stuff are

The lowest of low balls: an object with Int values:

object NotReallyAnEnum {
  val FIRST = 1
  val SECOND = 2
}

and a GADT

sealed trait NotReallyAnEnumEither
case object First extends NotReallyAnEnumEither
case object Seconds extends NotReallyAnEnumEither

In library support within the stdlib it also offers Enumeration which really falls short of being useful, the non-stdlib more general Shapeless, and julienrf’s enums.

The use cases that aren’t supported by these, but are supported by Enumeraturm are

  • Out of the box conversion Int <-> Enumartion type <-> String
  • Elements can be enumerated in the order they are declared in
  • People coming from other languages are used to having an enum construct

The first two have use cases, but how common are they really?

The last one, I actually think is a counter-indication. Scala has no enum, because it has other ways to do most of the same thing, and those other ways are in most cases sufficient. Providing Enumeratum because people expect enums runs the risk of leading people to believe that you should use Enumeratum for everything you used an enum in (language x with enums).

When you squint, the question “why doesn’t have scala something equivalent to enum” (be it in the language or as a library) looks a bit like “why doesn’t Java have something like goto”. Both have reasonable use cases, but the use cases where it’s a good idea are fewer than one would expect coming from a different language where the construct was an important part of the language. Of course, that comparison limps a bit - you want to be able to enumerate the inhabitants of some type far more often than you want to goto somewhere in an imperative language.

The question posed to the SPP committee seems to boil down to whether the use cases are common enough to facilitate with such a library in the platform at the cost of giving the possibly false impression that it’s the right thing to do when someone would have reached for an enum in (language x).


#16

Basically were I work (and have worked), a sizable amount of our code directly deals with stuff, so it definitely falls into the common bucket (if not very common).

At least when you are coding backend servers, and have to deal with any enum type that you have to serialize/parsing (as JSON, XML, or to a database), you need this functionality. This kind of "serializing/parsing to a database/json/xml) is a huge portion of typical backend webserver business logic code.

If you have something like

sealed abstract class Color(id: String)

case object Red extends Color("red")
case object Green extends Color("green")
case object Blue extends Color("blue")

And you want to parse this from JSON, you need some way to go from String representation to the actual type. The only way to do this currently is to use Java enums (which have their own limitations, Java Enum members don’t have their own types and it doesn’t work for Scala.js) or to manually write code like this

sealed abstract class Color(id: String)

case object Red extends Color("red")
case object Green extends Color("green")
case object Blue extends Color("blue")

val all = Set(Red, Green, Blue)
def withName(id: String) = all.find(_.id = id)

Unfortunately the problem is if your Enum has 30 options instead of just 3, defining all becomes a this big annoyance, and the boilerplate end up being violating the DRY principle really fast. Enumeratum at its core is a macro that defines the val all plus adds some extra functionality.


#17

Enums aren’t as central to my requirements as they are for mdedetrich – and yet, I still have found myself with a dozen or so of them in my codebase. And frankly, the chaotic status quo has encouraged ad-hockery in my code: I’ve tried all the alternatives at one time or another, and been dissatisfied with all of them.

Yes, enumerations are essentially the degenerate case of an ADT. But they’re a really common degenerate case, and it’s convenient to have a common technique for reducing the omnipresent boilerplate around them.

So personally, I like the idea of having Enumeratum (or at least, some library that fills this ecological niche) in the Platform – a good common technique for this problem would be helpful. (And indeed, I’ve recently picked up Enumeratum as the standard for my code going forward for this reason.) It seems like exactly the sort of thing that’s appropriate to the Platform: it fulfills a common, non-domain-specific requirement, while not being so central as to belong in the Standard Library…


#18

Having watched some of the SPP meeting (thanks @mdedetrich and others who spoke on behalf of the lib … sadly I can’t attend them myself), my fear is that people don’t fully understand what core problems Enumeratum solves, but are at the same time jumping to discussing alternative technical solutions.

Yes, it boils down to “deserialising to sum types”, but there are subtle real-world requirements/patterns to Enums that shouldn’t be overlooked, otherwise we risk providing users with something half-baked, and they end up disappointed and going elsewhere anyways.

So, here they are in point form as functional requirements, but in specific, I’d like to focus on 2: (bolded)

  1. Allow for a type-safe enum
  2. Ability to deserialise String values into the types of an ADT
  • This requires the ability to conjure a collection of your ADT types.
  1. Ability to guarantee declaration ordering
  2. Deserialising from non-String values
  3. Compile-time checks for value uniqueness

I think by now, many people are convinced that the first and second are worth solving. Arbitrary-ordering is also a fairly common requirement (ever need to provide some kind of list in a UI with arbitrary ordering? like…country, or gender…), and being forced to assign values to each member in code increases surface area for errors. And deserialising non-String values…well, anyone who has the good fortune of often working with legacy codebases, legacy databases, web APIs, or any situation where I/O bandwidth or performance is a concern would find this helpful.

BTW, if we recognise the need for 4 (deserialising from non-Strings), we must solve 4.1(compile-time checks for value uniqueness), because we can no longer assume the compiler will do identifier-based uniqueness checks for us. Otherwise, we get in to this unfortunate situation

sealed abstract class HttpStatus(val value: Int)

object HttpStatus {
  // MyGenericValueEnum is an alternative sum-type list-maker summoner that is not
  // Enumeratum.
  // 
  // I assume nobody wants to create and instantiate these lists multiple times in their
  // code everywhere they deserialise (e.g. from JSON, DB, w/e, Query params),
  // so we'll hold a stable list in the companion object
  val values = MyGenericValueEnum[HttpStatus] 

  object Ok        extends HttpStatus(200)
  object Created   extends HttpStatus(200)
  // etc
  object Forbidden extends HttpStatus(403)
  object NotFound  extends HttpStatus(403) // <--- this sort of thing should not compile
  // ...
}

// clearly we can do better

I believe that in order to solve all the requirements, the implementation must be specialised. In other words, you would necessarily end up writing a custom macro (or you add to the language/compiler directly.), as opposed to building on top of something like Shapeless’s Generic, because (and I’m glad to be proven wrong), you can’t. On top of that, there is the issue of emitting nice, relevant compiler error messages …

For what it’s worth, Enumeratum has been around for years, solving a very specific problem in an extensive way, even before it was reliable or feasible for other libraries. It’s got high adoption, has been vetted by 3rd parties (1, 2, 3), and has solid integrations with popular libraries.

These qualities, plus the fact that it solves the aforementioned core problems, means that offers a “batteries-included” experience for beginners, and yet scales to the needs of advanced/niche users, making it a good fit for the Scala Platform (bias warning :wink: )


#19

FWIW here’s what I do in a particular large project:

    // A simple typeclass
    class Bitmapped[A](val toBit: A => Int, val fromBit: Int => A)
    object Bitmapped {
      def apply[A](as: Seq[A]): Bitmapped[A] = Bitmapped[A](as.zipWithIndex: _*)
      def apply[A](as: (A, Int)*)(implicit name: Name): Bitmapped[A] =
        new Bitmapped[A](
          toBit = a => as.collectFirst { case (`a`, i) => i }.getOrElse(sys.error(s"$name.toBit: no value for $a")),
          fromBit = i => as.collectFirst { case (a, `i`) => a }.getOrElse(sys.error(s"$name.fromBit: no value for $i"))
        )
    }

    // An example usage
    object Priorities {
      sealed trait Priority
      case object Routine extends Priority
      case object Urgent extends Priority
      case object Emergency extends Priority

      val all = Seq(Routine, Urgent, Emergency)

      implicit val bitmapped: Bitmapped[Priority] = Bitmapped(Routine -> 0, Urgent -> 1, Emergency -> 2)
    }

Then, for serialization, I can define a serializer for Bitmapped things once, for example with Slick:

      implicit def bitmappedColumnType[A: ClassTag](implicit bitmapped: Bitmapped[A], ti: BaseColumnType[Int]): BaseColumnType[A] =
        MappedColumnType.base[A, Int](bitmapped.toBit, bitmapped.fromBit)
      implicit def bitmappedSetColumnType[A: ClassTag](implicit bitmapped: Bitmapped[A], ti: BaseColumnType[Long]): BaseColumnType[Set[A]] =
        MappedColumnType.base[Set[A], Long](
          as => BitSet(as.toSeq.map(bitmapped.toBit): _*).toBitMask.head,
          bm => BitSet.fromBitMask(Array(bm)).map(bitmapped.fromBit)
        )

if I ever need to map to/from a string, I just search all with toString.


#20

Hello @lloydmeta,

Thank you for providing those arguments. I think I have a clearer idea of what Enumeratum brings to the table, and I’m sure that all Committee members will find this information handy.

Yes. From your explanation before, it seems that Enumeratum is useful both for people learning Scala and seasoned Scala developers. I think the addition of this library could fill in an important gap and we underestimate the amount of people that require it. The following Reddit discussion seems to prove the point that the previously described functional requirements are requested by Scala developers. I defend this position even though I do not need enumeration in the daily Scala code I write, but I’m surely not the best example of normal Scala developers working in industry and dealing with real-world problems. I’m interested in increasing Scala adoption and providing a better hands-on experience – that’s why I believe this is a meaningful addition to the Scala Platform.

For now, the decision to add Enumeratum to the Scala Platform Incubator has been put off until our next Scala Platform meeting. I think that library authors bring invaluable experience to the Committee by commenting on design decisions/trade-offs that we miss and highly enrich discussions. That’s why I’d like to invite you to join our next meeting. If you’re up to it, please let me know and I’ll send you all the details.

I see a lot of good proposed ideas to improve Enumeratum (the typeclass approach that does not involve inheritance, for instance). I hope that if the library gets incubated, we (the community) can all work together on future improvements of the library. Some issues mentioned before are not fundamental to the design of the library and can be indeed implemented in a PR :wink:.