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

Для моего проекта я внедрил 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
}

from Объекты объекта vs Перечисления в Scala. Я работал неплохо, пока не столкнулся с проблемой. Объекты объекта кажутся ленивыми, и если я использую Currency.value, я могу получить пустой список. Было бы возможно сделать вызов против всех значений Enum при запуске, чтобы список значений был заполнен, но это было бы как бы победить точку.

Поэтому я отважился в темные и неизвестные места отражения scala и придумал это решение, основываясь на следующих ответах SO. Могу ли я получить список времени для всех объектов 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.

Ответ 1

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

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 за счет большей сложности, но, надеюсь, это поможет вам начать.

Ответ 2

Поздний ответ, но в любом случае...

Как сказал wallnuss, knownDirectSubclasses ненадежен, как написание, и был довольно долгое время.

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

Ответ 3

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

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

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

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

  • Замените класс Status классом sealed Status, подходящим для использования в качестве базового типа для значений перечисления. В частности, он вырастет конструктор (val index: Int, val name: String). Эти параметры будут предоставлены макросом, поэтому вам не придется беспокоиться об этом.
  • Сгенерируйте объект-компаньон Status, который будет содержать большинство частей, которые теперь делают 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