9

I want to have a simple enumDescr function for any Scala 3 enum.

Example:

  @description(enumDescr(InvoiceCategory))
  enum InvoiceCategory:
    case `Travel Expenses`
    case Misc
    case `Software License Costs`

In Scala 2 this is simple (Enumeration):

def enumDescr(enum: Enumeration): String =
  s"$enum: ${enum.values.mkString(", ")}"

But how is it done in Scala 3:

def enumDescr(enumeration: ??) = ...
pme
  • 14,156
  • 3
  • 52
  • 95

3 Answers3

10

Java Reflection

If you declare your enum as Java-compatible, you can use Java reflection to get the array of its values using the Class.getEnumConstants method.

To declare a Java-compatible enum it has to extend the Enum class:

enum Color extends Enum[Color]:
  case Red, Green, Blue

You can use getEnumConstants on the static class to get correctly typed Array of values:

val values: Array[Color] = classOf[Color].getEnumConstants

But if you want to use it in a generic manner, I believe you have to cast the Class or the Array to the correct type with asInstanceOf:

def enumValues[E <: Enum[E] : ClassTag]: Array[E] =
  classTag[E].runtimeClass.getEnumConstants.asInstanceOf[Array[E]]

Subtype declaration <: Enum[E] is not strictly needed there, but is used to avoid calling it with unrelated classes and causing runtime exceptions.

Now a method enumDescr can be written in a similar way:

def enumDescr[E <: Enum[E] : ClassTag]: String =
  val cl = classTag[E].runtimeClass.asInstanceOf[Class[E]]
  s"${cl.getName}: ${cl.getEnumConstants.mkString(", ")}"

And called like this:

scala> enumDescr[Color]
val res0: String = Color: Red, Green, Blue

Scala compile-time reflection

If you want just the names of the enum cases, you can get them using scala.deriving.Mirror (thanks to @unclebob for the idea):

import scala.deriving.Mirror
import scala.compiletime.{constValue, constValueTuple}

enum Color:
  case Red, Green, Blue

inline def enumDescription[E](using m: Mirror.SumOf[E]): String =
  val name = constValue[m.MirroredLabel]
  val values = constValueTuple[m.MirroredElemLabels].productIterator.mkString(", ")
  s"$name: $values"

@main def run: Unit =
  println(enumDescription[Color])

This prints:

Color: Red, Green, Blue

Scala 3 macro for a sequence of values

You can use Scala 3 macro to generate the call to values on the companion object.

Macro definitions from the same file can't be called, so the macro has to be placed in a separate file:

/* EnumValues.scala */

import scala.quoted.*

inline def enumValues[E]: Array[E] = ${enumValuesImpl[E]}

def enumValuesImpl[E: Type](using Quotes): Expr[Array[E]] =
  import quotes.reflect.*
  val companion = Ref(TypeTree.of[E].symbol.companionModule)
  Select.unique(companion, "values").asExprOf[Array[E]]

Then in the main file:

enum Color:
  case Red, Green, Blue

// Usable from `inline` methods:
inline def genericMethodTest[E]: String =
  enumValues[E].mkString(", ")

@main def run: Unit =
  println(enumValues[Color].toSeq)
  println(genericMethodTest[Color])
Kolmar
  • 14,086
  • 1
  • 22
  • 25
4

I don't see any common trait shared by all enum companion objects.

You still can invoke the values reflectively, though:

import reflect.Selectable.reflectiveSelectable

def descrEnum(e: { def values: Array[?] }) = e.values.mkString(",")

enum Foo:
  case Bar
  case Baz

descrEnum(Foo) // "Bar,Baz"
Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93
2

You can create an inline def using a Mirror.SumOf[A] which has all the information you need.

https://docs.scala-lang.org/scala3/reference/contextual/derivation.html

unclebob
  • 109
  • 7