Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create a general method for Scala 3 enums

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: ??) = ...
like image 582
pme Avatar asked Oct 27 '21 18:10

pme


People also ask

How do you create an enum in Scala?

In Scala, there is no enum keyword unlike Java or C. Scala provides an Enumeration class which we can extend in order to create our enumerations. Every Enumeration constant represents an object of type Enumeration. Enumeration values are defined as val members of the evaluation.

Can you have methods in enums?

The enum class body can include methods and other fields. The compiler automatically adds some special methods when it creates an enum. For example, they have a static values method that returns an array containing all of the values of the enum in the order they are declared.

Can enums have user defined methods?

Below code uses enums with defined methods: We should define methods as abstract methods and then we have to implement defferent flavours/logic based on each enum members. Because of declaring abstract method at the enum level; all of the enum members require to implement the method.

How do you use enums correctly?

You should always use enums when a variable (especially a method parameter) can only take one out of a small set of possible values. Examples would be things like type constants (contract status: “permanent”, “temp”, “apprentice”), or flags (“execute now”, “defer execution”).

How do you use enumerations in Scala?

Enumerations are useful tool for creating groups of constants, such as days of the week, months of the year, and many other situations where you have a group of related, constant values. You can also use the following approach of using a Scala trait to create the equivalent of a Java enum.

Does Scala 3 enumeration suffer from Boilerplate?

It does suffer from some of the boilerplate seen with the regular Scala Enumeration too. Scala 3 has introduced a new enum keyword. Our direction enumeration can now be constructed like this:

What's new in Scala 3?

Scala 3 has introduced a new enum keyword. Our direction enumeration can now be constructed like this: We have removed most of the boilerplate and unintuitive syntax: we are not extending a class, we don’t need to assign a type to the value, and we don’t need to assign our values. Similarly, we can add instance data to the enumerated instances:

How to use Scala-defined enums as Java enums?

If you want to use the Scala-defined enums as Java enums, you can do so by extending the class java.lang.Enum, which is imported by default, as follows: The type parameter comes from the Java enum definition and should be the same as the type of the enum.


Video Answer


2 Answers

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])
like image 179
Kolmar Avatar answered Nov 15 '22 06:11

Kolmar


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"
like image 30
Andrey Tyukin Avatar answered Nov 15 '22 07:11

Andrey Tyukin