Search code examples
scalascala-3

Enum with Parameters to Scala 3 Causes Serialization Errors


Consider the following code:

enum BrandSafetyFeatureStatus(val value: Long):
  case Inclusion          extends BrandSafetyFeatureStatus(1L << 0)
  case Exclusion          extends BrandSafetyFeatureStatus(1L << 1)
  case Exception          extends BrandSafetyFeatureStatus(1L << 2)

@main def main(): Unit = {
  println(BrandSafetyFeatureStatus.values)
}

Which works great. My question is why if I add new case with parameters or just with ():

enum BrandSafetyFeatureStatus(val value: Long):
  case Inclusion          extends BrandSafetyFeatureStatus(1L << 0)
  case Exclusion          extends BrandSafetyFeatureStatus(1L << 1)
  case Exception          extends BrandSafetyFeatureStatus(1L << 2)
  private case Mixed(mixed: Long) extends BrandSafetyFeatureStatus(mixed) // ⚠️

  // Add Bitwise
  def |(other: BrandSafetyFeatureStatus): BrandSafetyFeatureStatus = Mixed(value | other.value)
  def hasFlag(flag: BrandSafetyFeatureStatus): Boolean = (value & flag.value) == flag.value

@main def main(): Unit = {
  println(BrandSafetyFeatureStatus.valueOf("X")) // Not compiles
  println(BrandSafetyFeatureStatus.values) // Not compiles
}

The serialization function of values and valueOf generate me an error of:

value values is not a member of object scala3.BrandSafetyFeatureStatus. Although class BrandSafetyFeatureStatus is an enum, it has non-singleton cases, meaning a values array is not defined 

Is it by design? is it an open bug? I know that with enumeratum I can solve it as follows:

sealed class BrandSafetyFeatureStatus(val value: Long) extends EnumEntry {
  def | (other: BrandSafetyFeatureStatus): BrandSafetyFeatureStatus = BrandSafetyFeatureStatus(value | other.value)
  def hasFlag(flag: BrandSafetyFeatureStatus): Boolean = (value & flag.value) == flag.value
}

object BrandSafetyFeatureStatus extends Enum[BrandSafetyFeatureStatus]:
  case object Inclusion         extends BrandSafetyFeatureStatus(1L << 1)
  case object Exclusion         extends BrandSafetyFeatureStatus(1L << 2)
  case object Exception         extends BrandSafetyFeatureStatus(1L << 3)

  override def values: IndexedSeq[BrandSafetyFeatureStatus] = findValues

@main def main(): Unit = {
  println(BrandSafetyFeatureStatus.withName("X"))
  println(BrandSafetyFeatureStatus.values)
}

Solution

  • If you have a parameterless case like this:

    enum Foo:
      case Bar
      case Baz
    

    The fact that these casees are parameterless means that you can create the set of all possible values:

    val values = Set(Foo.Bar, Foo.Baz)
    

    When you define

    enum Foo:
      case Bar
      case Baz
      case Third(something: Int)
    

    How could you define values upfront? Well, you cannot. Enumeratum is a library which creates such set of values and use it to provide some functionalities:

    • convert between reference (there is only 1 instance of each value) and some string with its name
    • convert between reference and its ordinal
    • etc

    It achieves it by finding all subtypes of enum, verifying that each of them in a singleton with only 1 value, putting these values into values (all of that so far with findValues macro, and you putting this macro after val values = ), and finally taking that values list and iterating over it to produce Strings and Ints with some helper function.

    This cannot be done if any of the casees is not a value. Even empty parameter list (e.g. case Value()) would mean that when you do Value() you are calling a constructor, creating a new instance, with a new reference, and the mechanism above is not accessible anymore. findValues for enum with parameters will just fail in compilation.

    Serialization of parameterless values is trivial, because there is always an easy way to translate each value to some string - namely the value assigned by Enumeratum as the name (it can be customized).

    What when you need to parse it somehow? (To figure what values pass into these parameters). Well, then there is no single, generic behavior, that could be assumed to always be safe to use. In such case you'd have to read how your particular library handles (de)serialization of ADTs: does it use discrimination field, or some fallback behavior, or something else. (Enumeratun on its own is not a serialization library, but it has integrations with some of them).

    So it's by design.