Могу ли я получить список времени компиляции всех объектов case, которые получены из запечатанного родителя в Scala?

Как неоднократно обсуждалось на SO, соответствие Scala предупредит вас, если вы не исчерпывающе перечислите все типы, полученные из закрытого класса.

То, что я хочу, - это сгенерированный с помощью компиляции Iterable объектов case, происходящих от определенного родителя. В качестве альтернативы, я был бы доволен тем, как заставить компилятор сказать мне, что у меня нет всех необходимых типов в некоторых Iterable. Мне не нужен подход, основанный на анализе на основе времени выполнения.

В качестве примера второго подхода я хотел бы, чтобы следующий грубый код генерировал ошибку компиляции там, где это указано.

sealed trait Parent
case object A extends Parent
case object B extends Parent
case object C extends Parent

// I want a compiler error here because C is not included in the Seq()
val m = Seq(A, B).map(somethingUseful)

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

Размышляя об этом по-другому, я бы взял что-то вроде метода Enumeration.values ​​(), кроме случаев, примененных к объектам case. Конечно, я мог бы добавить что-то похожее на приведенный выше код с сохраненным вручную списком значений в родительский объект-компаньон, но это кажется бесполезным для ошибок, когда компилятор может это сделать для меня.

// Manually maintained list of values
object Parent { 
    val values = Seq(A, B, C)
}

Ответ 1

Update. Начиная с версии 2.10.0-M7 мы раскрываем методы, упомянутые в этом ответе, как часть общедоступного API. isSealed - ClassSymbol.isSealed, а sealedDescendants - ClassSymbol.knownDirectSubclasses.

Это не будет ответом на ваш вопрос.

Но, если вы готовы согласиться на что-то большее, как Enumeration.values(), и вы используете недавнюю веху в 2.10, и вы готовы гадать с каким-то уродливым бизнесом от литья к внутренним интерфейсам, вы можете написать следующее:

import scala.reflect.runtime.universe._

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

Теперь, если у вас есть такая иерархия:

object Test {
  sealed trait Parent
  case object A extends Parent
  case object B extends Parent
  case object C extends Parent
}

Вы можете получить символы типа для элементов иерархии закрытого типа, например:

scala> sealedDescendants[Test.Parent] getOrElse Set.empty
res1: Set[reflect.runtime.universe.Symbol] = Set(object A, object B, object C)

Это отвратительно, но я не думаю, что вы получите то, что действительно хотите, не написав плагин компилятора.

Ответ 2

Вот рабочий пример с использованием макросов на 2.10.0-M6:

(обновление: чтобы этот пример работал в 2.10.0-M7, вам нужно заменить c.TypeTag на c.AbsTypeTag; чтобы этот пример работал в 2.10.0-RC1, c.AbsTypeTag необходимо заменить на c.WeakTypeTag)

import scala.reflect.makro.Context

object SealednessMacros {
  def exhaustive[P](ps: Seq[P]): Seq[P] = macro exhaustive_impl[P]

  def exhaustive_impl[P: c.TypeTag](c: Context)(ps: c.Expr[Seq[P]]) = {
    import c.universe._

    val symbol = typeOf[P].typeSymbol

    val seen = ps.tree match {
      case Apply(_, xs) => xs.map {
        case Select(_, name) => symbol.owner.typeSignature.member(name)
        case _ => throw new Exception("Can't check this expression!")
      }
      case _ => throw new Exception("Can't check this expression!")
    }

    val internal = symbol.asInstanceOf[scala.reflect.internal.Symbols#Symbol]    
    if (!internal.isSealed) throw new Exception("This isn't a sealed type.")

    val descendants = internal.sealedDescendants.map(_.asInstanceOf[Symbol])

    val objs = (descendants - symbol).map(
      s => s.owner.typeSignature.member(s.name.toTermName)
    )

    if (seen.toSet == objs) ps else throw new Exception("Not exhaustive!")
  }
}

Это, очевидно, не очень надежное (например, предполагается, что у вас есть только объекты в иерархии, и он будет терпеть неудачу на A :: B :: C :: Nil), и он по-прежнему требует некоторого неприятного кастинга, но он работает быстро проверка концепции.

Сначала мы скомпилируем этот файл с включенными макросами:

scalac -language:experimental.macros SealednessMacros.scala

Теперь, если мы попытаемся скомпилировать такой файл:

object MyADT {
  sealed trait Parent
  case object A extends Parent
  case object B extends Parent
  case object C extends Parent
}

object Test extends App {
  import MyADT._
  import SealednessMacros._

  exhaustive[Parent](Seq(A, B, C))
  exhaustive[Parent](Seq(C, A, B))
  exhaustive[Parent](Seq(A, B))
}

Мы получим ошибку времени компиляции на Seq с отсутствующим C:

Test.scala:14: error: exception during macro expansion: 
java.lang.Exception: Not exhaustive!
        at SealednessMacros$.exhaustive_impl(SealednessMacros.scala:29)

  exhaustive[Parent](Seq(A, B))
                    ^
one error found

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