Custom Scala enum, most elegant version searched

馋奶兔 提交于 2019-11-27 22:13:53

问题


For a project of mine I have implemented a Enum based upon

trait Enum[A] {
  trait Value { self: A =>
    _values :+= this
  }
  private var _values = List.empty[A]
  def values = _values
}

sealed trait Currency extends Currency.Value
object Currency extends Enum[Currency] {
  case object EUR extends Currency
  case object GBP extends Currency
}

from Case objects vs Enumerations in Scala. I worked quite nice, till I run into the following problem. Case objects seem to be lazy and if I use Currency.value I might actually get an empty List. It would have been possible to make a call against all Enum Values on startup so that the value list would be populated, but that would be kind of defeating the point.

So I ventured into the dark and unknown places of scala reflection and came up with this solution, based upon the following SO answers. Can I get a compile-time list of all of the case objects which derive from a sealed parent in Scala? and How can I get the actual object referred to by Scala 2.10 reflection?

import scala.reflect.runtime.universe._

abstract class Enum[A: TypeTag] {
  trait Value

  private def sealedDescendants: Option[Set[Symbol]] = {
    val symbol = typeOf[A].typeSymbol
    val internal = symbol.asInstanceOf[scala.reflect.internal.Symbols#Symbol]
    if (internal.isSealed)
      Some(internal.sealedDescendants.map(_.asInstanceOf[Symbol]) - symbol)
    else None
  }

  def values = (sealedDescendants getOrElse Set.empty).map(
    symbol => symbol.owner.typeSignature.member(symbol.name.toTermName)).map(
    module => reflect.runtime.currentMirror.reflectModule(module.asModule).instance).map(
    obj => obj.asInstanceOf[A]
  )
}

The amazing part of this is that it actually works, but it is ugly as hell and I would be interested if it would be possible to make this simpler and more elegant and to get rid of the asInstanceOf calls.


回答1:


Here is a simple macro based implementation:

import scala.language.experimental.macros
import scala.reflect.macros.blackbox

abstract class Enum[E] {
  def values: Seq[E] = macro Enum.caseObjectsSeqImpl[E]
}

object Enum {
  def caseObjectsSeqImpl[A: c.WeakTypeTag](c: blackbox.Context) = {
    import c.universe._

    val typeSymbol = weakTypeOf[A].typeSymbol.asClass
    require(typeSymbol.isSealed)
    val subclasses = typeSymbol.knownDirectSubclasses
      .filter(_.asClass.isCaseClass)
      .map(s => Ident(s.companion))
      .toList
    val seqTSymbol = weakTypeOf[Seq[A]].typeSymbol.companion
    c.Expr(Apply(Ident(seqTSymbol), subclasses))
  }
}

With this you could then write:

sealed trait Currency
object Currency extends Enum[Currency] {
  case object USD extends Currency
  case object EUR extends Currency
}

so then

Currency.values == Seq(Currency.USD, Currency.EUR)

Since it's a macro, the Seq(Currency.USD, Currency.EUR) is generated at compile time, rather than runtime. Note, though, that since it's a macro, the definition of the class Enum must be in a separate project from where it is used (i.e. the concrete subclasses of Enum like Currency). This is a relatively simple implementation; you could do more complicated things like traverse multilevel class hierarchies to find more case objects at the cost of greater complexity, but hopefully this will get you started.




回答2:


A late answer, but anyways...

As wallnuss said, knownDirectSubclasses is unreliable as of writing and has been for quite some time.

I created a small lib called Enumeratum (https://github.com/lloydmeta/enumeratum) that allows you to use case objects as enums in a similar way, but doesn't use knownDirectSubclasses and instead looks at the body that encloses the method call to find subclasses. It has proved to be reliable thus far.




回答3:


The article "“You don’t need a macro” Except when you do" by Max Afonov maxaf describes a nice way to use macro for defining enums.

The end-result of that implementation is visible in github.com/maxaf/numerato

Simply create a plain class, annotate it with @enum, and use the familiar val ... = Value declaration to define a few enum values.

The @enum annotation invokes a macro, which will:

  • Replace your Status class with a sealed Status class suitable for acting as a base type for enum values. Specifically, it'll grow a (val index: Int, val name: String) constructor. These parameters will be supplied by the macro, so you don't have to worry about it.
  • Generate a Status companion object, which will contain most of the pieces that now make Status an enumeration. This includes a values: List[Status], plus lookup methods.

Give the above Status enum, here's what the generated code looks like:

scala> @enum(debug = true) class Status {
     |   val Enabled, Disabled = Value
     | }
{
  sealed abstract class Status(val index: Int, val name: String)(implicit sealant: Status.Sealant);
  object Status {
    @scala.annotation.implicitNotFound(msg = "Enum types annotated with ".+("@enum can not be extended directly. To add another value to the enum, ").+("please adjust your `def ... = Value` declaration.")) sealed abstract protected class Sealant;
    implicit protected object Sealant extends Sealant;
    case object Enabled extends Status(0, "Enabled") with scala.Product with scala.Serializable;
    case object Disabled extends Status(1, "Disabled") with scala.Product with scala.Serializable;
    val values: List[Status] = List(Enabled, Disabled);
    val fromIndex: _root_.scala.Function1[Int, Status] = Map(Enabled.index.->(Enabled), Disabled.index.->(Disabled));
    val fromName: _root_.scala.Function1[String, Status] = Map(Enabled.name.->(Enabled), Disabled.name.->(Disabled));
    def switch[A](pf: PartialFunction[Status, A]): _root_.scala.Function1[Status, A] = macro numerato.SwitchMacros.switch_impl[Status, A]
  };
  ()
}
defined class Status
defined object Status


来源:https://stackoverflow.com/questions/20089920/custom-scala-enum-most-elegant-version-searched

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!