Search code examples
scalapattern-matchingtypeclassimplicit

Type class instance for case objects defined in sealed trait


In Scala 2.13 I have a case where I define some operation for all of types extending some sealed trait EnumType. I made it working but I'd like getTypeClass function not to be dependant on concrete types extending EnumType. Now I have to visit this function any time EnumType changes and add or remove some pattern. Is there a way to get instances of Operation type class for EnumType types but without pattern matching on all of them?

  sealed trait EnumType
  case object Add10 extends EnumType
  case object Add50 extends EnumType

  trait Operation[+T] {
    def op(a: Int): Int
  }

  implicit val add10: Operation[Add10.type] = (a: Int) => a + 10
  implicit val add50: Operation[Add50.type] = (a: Int) => a + 50

  def getTypeClass(enumType: EnumType): Operation[EnumType] = {
    // I need to modify this pattern matching
    // every time EnumType changes
    enumType match {
      case Add10 => implicitly[Operation[Add10.type]]
      case Add50 => implicitly[Operation[Add50.type]]
    }

    // I'd wish it could be done with without pattern matching like (it does not compile):
    //   implicitly[Operation[concrete_type_of(enumType)]]
  }

  // value of enumType is dynamic - it can be even decoded from some json document
  val enumType: EnumType = Add50
  println(getTypeClass(enumType).op(10)) // prints 60

EDIT That's the way I wish it was called without using explicit subtypes of EnumType (using circe in this example to decode json) :

  case class Doc(enumType: EnumType, param: Int)

  implicit val docDecoder: Decoder[Doc] = deriveDecoder
  implicit val enumTypeDecoder: Decoder[EnumType] = deriveEnumerationDecoder

  decode[Doc]("""{"enumType": "Add10", "param": 10}""").map {
    doc =>
      println(getTypeClass(doc.enumType).call(doc.param))
  }

Solution

  • Since you only know that statically enumType has type just EnumType and want to match based on runtime value/runtime class of enumType, you'll have to use some kind of reflection:

    // libraryDependencies += scalaOrganization.value % "scala-compiler" % scalaVersion.value
    import scala.reflect.runtime.{currentMirror => cm}
    import scala.reflect.runtime.universe._
    import scala.tools.reflect.ToolBox
    val tb = cm.mkToolBox()
      
    def getTypeClass(enumType: EnumType): Operation[EnumType] =
      tb.eval(q"_root_.scala.Predef.implicitly[Operation[${cm.moduleSymbol(enumType.getClass)}]]")
        .asInstanceOf[Operation[EnumType]]
    

    or

    def getTypeClass(enumType: EnumType): Operation[EnumType] =
      tb.eval(tb.untypecheck(tb.inferImplicitValue(
        appliedType(
          typeOf[Operation[_]].typeConstructor,
          cm.moduleSymbol(enumType.getClass).moduleClass.asClass.toType
        ),
        silent = false
      ))).asInstanceOf[Operation[EnumType]]
    

    or

    def getTypeClass(enumType: EnumType): Operation[EnumType] = {
      val cases = typeOf[EnumType].typeSymbol.asClass.knownDirectSubclasses.map(subclass => {
        val module = subclass.asClass.module
        val pattern = pq"`$module`"
        cq"$pattern => _root_.scala.Predef.implicitly[Operation[$module.type]]"
      })
      tb.eval(q"(et: EnumType) => et match { case ..$cases }")
        .asInstanceOf[EnumType => Operation[EnumType]]
        .apply(enumType)
    }
    
    • or a macro (automating the pattern matching)
    import scala.language.experimental.macros
    import scala.reflect.macros.blackbox
    
    def getTypeClass(enumType: EnumType): Operation[EnumType] = macro getTypeClassImpl
    
    def getTypeClassImpl(c: blackbox.Context)(enumType: c.Tree): c.Tree = {
      import c.universe._
      val cases = typeOf[EnumType].typeSymbol.asClass.knownDirectSubclasses.map(subclass => {
        val module = subclass.asClass.module
        val pattern = pq"`$module`"
        cq"$pattern => _root_.scala.Predef.implicitly[Operation[$module.type]]"
      })
      q"$enumType match { case ..$cases }"
    }
    
    //scalac: App.this.enumType match {
    //  case Add10 => _root_.scala.Predef.implicitly[Macro.Operation[Add10.type]]
    //  case Add50 => _root_.scala.Predef.implicitly[Macro.Operation[Add50.type]]
    //}
    

    Since all the objects are defined at compile time I guess that a macro is better.

    Covariant case class mapping to its base class without a type parameter and back

    Getting subclasses of a sealed trait

    Iteration over a sealed trait in Scala?

    You can even make a macro whitebox, then using runtime reflection in the macro you can have Add50 type statically (if the class is known during macro expansion)

    import scala.language.experimental.macros
    import scala.reflect.macros.whitebox
    
    def getTypeClass(enumType: EnumType): Operation[EnumType] = macro getTypeClassImpl
    
    def getTypeClassImpl(c: whitebox.Context)(enumType: c.Tree): c.Tree = {
      import c.universe._
      val clazz = c.eval(c.Expr[EnumType](c.untypecheck(enumType))).getClass
      val rm = scala.reflect.runtime.currentMirror
      val symbol = rm.moduleSymbol(clazz)
      //q"_root_.scala.Predef.implicitly[Operation[${symbol.asInstanceOf[ModuleSymbol]}.type]]" // implicit not found
      //q"_root_.scala.Predef.implicitly[Operation[${symbol/*.asInstanceOf[ModuleSymbol]*/.moduleClass.asClass.toType.asInstanceOf[Type]}]]" // implicit not found
        // "migrating" symbol from runtime universe to macro universe
      c.parse(s"_root_.scala.Predef.implicitly[Operation[${symbol.fullName}.type]]")
    }
    
    object App {
      val enumType: EnumType = Add50
    }
    
    val operation = getTypeClass(App.enumType)
    operation: Operation[Add50.type] // not just Operation[EnumType]
    operation.op(10) // 60
    

    How to accept only a specific subtype of existential type?

    In a scala macro, how to get the full name that a class will have at runtime?