Пользовательский Scala enum, самая элегантная версия

для моего проекта я реализовал перечисление, основанное на

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
}

С объекты Case vs перечисления в Scala. Я работал довольно хорошо, пока не столкнулся со следующей проблемой. Объекты Case кажутся ленивыми, и если я использую валюту.значение я мог бы на самом деле получить пустой список. Было бы возможно сделать вызов против всех значений перечисления при запуске, чтобы список значений был заполнен, но это было бы своего рода поражением точка.

могу ли я получить список времени компиляции всех объектов case, производных от запечатанного родителя в Scala? и как я могу получить фактический объект, на который ссылается в Scala 2.10 отражение?
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]
  )
}

удивительная часть этого заключается в том, что он действительно работает, но он уродлив, как ад, и мне было бы интересно, если можно было бы сделать это проще и элегантнее и избавиться от вызовов asInstanceOf.

3 ответов


вот простая реализация на основе макросов:

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))
  }
}

С этим вы могли бы написать:

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

вот тут

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

так как это макрос Seq(Currency.USD, Currency.EUR) генерируется во время компиляции, а не во время выполнения. Обратите внимание, что, поскольку это макрос, определение class Enum должен быть в отдельном проекте, где он используется (т. е. конкретные подклассы Enum как Currency). Это относительно простая реализация; вы могли бы сделать более сложные вещи, такие как пересечение многоуровневых иерархий классов, чтобы найти больше объектов case ценой большей сложности, но, надеюсь, это поможет вам начать.


поздний ответ, но все же...

Как wallnuss сказал:knownDirectSubclasses ненадежна, так как писать и уже довольно давно.

Я создал небольшой lib под названием Enumeratum (https://github.com/lloydmeta/enumeratum), что позволяет использовать объекты case в качестве перечислений аналогичным образом, но не использует knownDirectSubclasses и вместо этого смотрит на тело, которое заключает вызов метода для поиска подклассов. До сих пор она доказала свою надежность.


в статье " "вам не нужен макрос", за исключением случаев, когда вы делаете " by Макс Afonov maxaf описывает хороший способ использования макроса для определения перечислений.

конечный результат этой реализации виден в github.com/maxaf/numerato

просто создайте простой класс, аннотируйте его с помощью @enum, и используйте знакомый val ... = Value объявление для определения нескольких значений перечисления.

на @enum аннотация вызывает макрос, который будет:

  • заменить Status класс sealed Status класс подходит для действующий в качестве базового типа для значений enum. В частности, он будет расти (val index: Int, val name: String) конструктор. Эти параметры будут предоставлены макросом, поэтому вам не придется беспокоиться об этом.
  • создать Status companion объект, который будет содержать большинство частей, которые теперь делают Status перечисление. Это включает в себя значения: List[Status] поиск плюс методы.

дать выше Status enum, вот как выглядит сгенерированный код:

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