Pregunta ¿Puedo obtener una lista en tiempo de compilación de todos los objetos del caso que se derivan de un padre sellado en Scala?


Como se ha discutido muchas veces en SO, un emparejamiento de Scala le advertirá si no enumera exhaustivamente todos los tipos derivados de una clase sellada.

Lo que quiero es un Iterable generado en tiempo de compilación de los objetos de caso derivados de un padre particular. Alternativamente, estaría contento con una forma de hacer que el compilador me diga que no tengo todos los tipos necesarios en algún Iterable. No quiero un enfoque basado en la reflexión en tiempo de ejecución.

Como ejemplo del segundo enfoque, me gustaría que el siguiente código aproximado genere un error de compilación donde se indique.

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)

Siéntase libre de responder diciéndome que no es posible. Parece que debería ser posible en algún nivel porque el compilador debe estar haciendo esencialmente el mismo trabajo al determinar que una coincidencia no es exhaustiva.

Pensando en ello de otra manera, tomaría algo como el método Enumeration.values ​​(), excepto que se aplica a objetos de casos. Ciertamente, podría agregar algo similar al código anterior con una lista de valores mantenida manualmente al objeto complementario del padre, pero parece innecesariamente propenso a errores cuando el compilador podría hacer eso por mí.

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

32
2017-08-22 17:28


origen


Respuestas:


Actualizar. Desde 2.10.0-M7, estamos exponiendo los métodos mencionados en esta respuesta como parte de la API pública. isSealed es ClassSymbol.isSealed y sealedDescendants es ClassSymbol.knownDirectSubclasses.

Esta no va a ser una respuesta a tu pregunta.

Pero, si estás dispuesto a conformarte con algo más como Enumeration.values(), y estás usando un hito reciente de 2.10, y Si estás dispuesto a desaprovecharte con algunos feos negocios de conversión a API internas, puedes escribir lo siguiente:

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
}

Ahora si tienes una jerarquía como esta:

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

Puede obtener los símbolos de tipo para los miembros de la jerarquía de tipo sellado de esta manera:

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

Es horrible, pero no creo que vayas a obtener lo que realmente quieres sin escribir un plugin de compilación.


23
2017-08-22 20:39



Aquí hay un ejemplo de trabajo que usa macros en 2.10.0-M6:

(actualización: para que este ejemplo funcione en 2.10.0-M7, debe reemplazar c.TypeTag con c.AbsTypeTag; para que este ejemplo funcione en 2.10.0-RC1, c.AbsTypeTag debe reemplazarse con 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!")
  }
}

Obviamente, esto no es muy sólido (por ejemplo, supone que solo tiene objetos en la jerarquía y fallará A :: B :: C :: Nil), y aún requiere algunos lanzamientos desagradables, pero funciona como una prueba rápida de concepto.

Primero compilamos este archivo con macros habilitadas:

scalac -language:experimental.macros SealednessMacros.scala

Ahora, si tratamos de compilar un archivo como este:

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

Obtendremos un error en tiempo de compilación en Seq con la falta 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

Tenga en cuenta que tenemos que ayudar al compilador a salir con un parámetro de tipo explícito que indique el padre.


11
2017-08-27 01:07