我能否在Scala中获取所有从密封父级继承的case对象的编译时列表?

40

正如在SO上多次讨论的那样,如果您没有详尽列出所有从密封类派生的类型,Scala匹配将会发出警告。

我想要的是一个在编译时生成的从特定父类继承的case对象的可迭代集合。或者,我希望有一种方式可以让编译器告诉我某个可迭代对象中缺少了必要的类型。我不想使用基于运行时反射的方法。

作为第二种方法的示例,我希望以下大致代码在指定位置生成编译错误。

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
“可迭代的 case classes” 是什么意思?Case class 是一种类型,而不是对象,因此无法存储它。一个可迭代的所有扩展密封类的对象集合更容易实现。 - Daniel C. Sobral
非常好的观点,@DanielC.Sobral。我已经相应地更新了问题。 - Leif Wickland
2个回答

26

更新。自2.10.0-M7版本以来,我们将本回答中提到的方法作为公共API的一部分进行了公开。 isSealedClassSymbol.isSealed,而sealedDescendantsClassSymbol.knownDirectSubclasses

这并不是对你问题的回答。

但是,如果你愿意接受类似于Enumeration.values()的更实用的解决方案,并且你正在使用最新的2.10版本,并且你愿意涉及一些难看的内部API转换操作,那么你可以编写以下代码:

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)

这看起来很丑,但我认为如果不编写编译器插件,你将无法得到实际想要的结果。


1
密封性通过ClassSymbol上的isSealed方法暴露出来。您可以通过在Symbol上调用asClass来获取它。 - Daniel C. Sobral
我刚刚检查了一下,它也被暴露出来了。虽然似乎没有起作用。 - Daniel C. Sobral
1
@LeifWickland,除非你采用Travis使用的相同技巧,否则不可能实现--isSealed是公开的,但sealedDescendants不是。 - Daniel C. Sobral
1
@LeifWickland:不,你应该能够通过宏得到一个编译时的解决方案,但你仍然需要将其转换为内部API。我会在这个周末尝试一下。 - Travis Brown
我该如何获取底层对象的引用以调用其方法? - Edmondo
显示剩余13条评论

13
以下是使用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))
}

如果缺少 C,我们在带有 Seq 的编译时会出现错误:

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

请注意,我们需要通过显式类型参数指示父级来帮助编译器。

1
您可以使用 c.error 和 c.abort 来报告错误。前者只是向编译器发出错误信号,而后者则调用 c.error 然后终止宏。 - Eugene Burmako
1
同样在M7中:1)c.TypeTag需要更改为c.AbsTypeTag,2)typeOf[T]需要变成implicitly[c.AbsTypeTag[T]].tpe,3)还有isSealed方法。 - Eugene Burmako
1
关于类型转换。您可以随时给我发一条消息,请求公开一些内部API。特别是因为反射API仍未冻结,我们添加的内容,比如今天,很可能最终会出现在2.10.0-final版本中。 - Eugene Burmako
感谢您抽出时间编写并发布这个示例,@TravisBrown。 - Leif Wickland
2
提交9abf74be15672ce4ec1900a6b26fbf35cbce5866引入了knownDirectSubclasses,这应该足以避免强制转换。我不知道它是否已经进入2.10。 - Daniel C. Sobral

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接