Pre-SIP: Make enum objects extend some useful trait

If I understand it correctly, an enum Color { case Red, Blue } is desugared into a plain (non-case) object with no extends: https://docs.scala-lang.org/scala3/reference/enums/desugarEnums.html

I have several times wanted to match on a value and check if it is an enum companion so that I know that it has e.g. the values member and can make use of it.

If the enum companion would extend a trait that let me match on the Color object I could get to the values if my x: Any is an enum companion.

Is there any downside in making the desugaring of an enum into trait+companion with the companion object extending a useful trait?

Or could it extend something that makes it possible to detect at runtime if a value is an enum object?

Or is there any other existing solution on how to match on a Color: Any object to see if it is indeed an enum object at runtime and get hold of it’s values member?

is this something that you are experimenting with, like in a REPL session, or this for a utility function that loads the values of an enum?

for the latter you can try enum-extensions library:

scala-cli --dep io.github.bishabosha::enum-extensions:0.1.1
Welcome to Scala 3.3.1 (21.0.1, Java OpenJDK 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.

scala> import enumextensions.EnumMirror

scala> inline given AnyEnumMirror[E <: reflect.Enum]: EnumMirror[E] = EnumMirror.derived
def AnyEnumMirror[E <: reflect.Enum]: enumextensions.EnumMirror[E]

scala> inline def enumValues[E]: IArray[E] = compiletime.summonFrom {
     |   case m: EnumMirror[E] => m.values
     |   case c: reflect.ClassTag[E] => IArray.empty
     | }
def enumValues[E]: IArray[E]

scala> enum Foo:
     |   case A, B, C
     | enum Bar:
     |   case X(x: Int)
     |
// defined class Foo
// defined class Bar

scala> enumValues[Foo]
val res0: IArray[Foo] = Array(A, B, C)

scala> enumValues[Bar]
val res1: IArray[Bar] = Array()

this way is still generic, rather than doing a runtime type check on a specific companion object

1 Like

The enumextensions library is really cool!

or this for a utility function

One use case is that I have a DSL of several enums and I want to pass any enum object to a function that iterates over their values.

this way is still generic, rather than doing a runtime type check

So I wonder if it would not be even simpler if enum desugaring made the enum object extend trait EnumObj { def values: Array[Enum] } or similar so that I can simply:

def iterate(x: EnumObj, f: Any => Result): Seq[Result] = 
  x.values.map(f).toSeq 
1 Like

You can try reflection?

import scala.reflect.Selectable.reflectiveSelectable

def iterate[EObj <: { def values: Array[? <: reflect.Enum] }, Result](x: EObj, f: Any => Result): Seq[Result] =
  x.values.toSeq.map(f)

scala> iterate(Foo, _.toString)
val res5: Seq[String] = ArraySeq(A, B, C)

right but you want to dynamically test that, you dont know ahead of time if it does

Cool! I can try that.

But if I don’t want to use reflection? (with those scary <: types that not every code reader knows about…)

Wouldn’t a simple extends on the enum object do the trick? Or is there a downside to extend something when desugaring?

it should be possible - we already add scala.deriving.Mirror.Sum, adding a new superclass/interface is supposed to be backwards compatible.

The problem is that now you will only get this on classes compiled since 3.5 or whatever, so not useful for libraries that may only be compiled once in the 3.0.0 days

1 Like

I can live with that. Eventually it will not be a problem… (I will not retire anytime soon :slight_smile: )

Do you think I should start (or rename this thread to) a Pre-SIP?

sure! others have asked for something similar a few times now, which is why I made the enum-extensions to cover the gap

1 Like

sure!

Did it! ; thread now renamed with Pre-SIP tag.

I need help from anyone here interested in the topic to elicit how the EnumObj trait should look like and what it should actually be called etc.

This page currently says:

sealed abstract class E ... extends <parents> with scala.reflect.Enum {
  import E.{ <caseIds> }
  <defs>
}
object E { <cases> }

And this Pre-SIP proposes that it is changed to something like:

sealed abstract class E ... extends <parents> with scala.reflect.Enum {
  import E.{ <caseIds> }
  <defs>
}
object E extends EnumCompanion { <cases> }

Where EnumCompanion looks something like:

trait EnumCompanion {
  def values: Array[E]
  def valueOf(name: String): E
  def fromOrdinal(ordinal: Int): E
}

I think perhaps EnumCompanion has less of a risk of being confused with the actual cases (which are also objects). Here are candidate names that I have brainstormed:

  • EnumCompanion
  • EnumType
  • EnumObj
  • EnumObject
  • EnumSingleton
1 Like

The idea looks neat in general! I like it.

Usability improvements for the most basic language features are always highly welcome.

Regarding the “useful trait”: How about making “EnumObjects” extend some EnumSet, a new collection type?

Enums are sets in the end. Just of some kind of specially treated values.

I would like to iterate, map, filter, etc. enum value sets. Also there could be overloaded applys for String and Int which return the matching EnumValue (typed more concretely of course). Would be cool to have this methods on the “EnumObjects”.

I’m not sure, but maybe it’s even quite simple to add an EnumSet type as there is already a concept of EnumValue in the compiler. It’s just synthetic at the moment. So EnumSet could be internally just a Set[EnumValue] if EnumValue would become a “real thing” (not only synthesised).

Should this be generic in the type of the enum?

trait EnumCompanion[E] {
  def values: Array[E]
  def valueOf(name: String): E
  def fromOrdinal(ordinal: Int): E
}

That way you can parameterize methods that need it like def foo[E](companion: EnumCompanion[E]): ???.

It might also be worth making EnumCompanion provide itself implicitly, so this could be done (which is done by scalapb and is really handy) def foo[E: EnumCompanion]: ???

This might be anathema to some, but I’d love it if Scala enums could be automatically sugared to conform to java.lang.Enum where possible. :slight_smile:

I mean, that already happens if the enum extends java.lang.Enum, does it not?

1 Like

the companion won’t extend anything useful no, and If you have some generic algorithm there isn’t a simple way to get the companion object of an arbitrary class where you don’t know its static type

See previous discussion on

2 Likes

@soronpo Thanks for the pointer. Would my simpler proposal above cover (large enough parts of) the use cases you envisioned in the previous discussions?

Yes, but I would look into the comments on my PR. There were some interesting suggestions there.

1 Like