Home > Net >  Type class instance for case objects defined in sealed trait
Type class instance for case objects defined in sealed trait

Time:10-08

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

CodePudding user response:

 def getTypeClass[T <: EnumType : Operation](t: T) = implicitly[Operation[T]]

 println(getTypeClass(Add50).op(10))

In fact, you don't even need getTypeClass at all:

def operate[T <: EnumType : Operation](t: T)(param: Int) = implicitly[Operation[T]].op(param)

println(operate(Add50)(10))

The Foo : Bar notation I used above is equivalent to this:

def operate[T <: EnumType](t: T)(param: Int)(op: Operation[T]) = op.op(param)

Note, that you are not actually using instances of Add50 and Add10 anywhere. They could just be traits rather than objects (IMO, that better reflects the intent):

  sealed trait EnumType
  trait Add10 extends EnumType
  trait Add50 extends EnumType

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

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

  def operate[T <: EnumType : Operation](param: Int) = 
   implicitly[Operation[T]].op(param)

 println(operate[Add50](10))

CodePudding user response:

Since you know that statically enumType has type just EnumType and you want to match based on runtime value of enumType, in order to automate pattern matching you'll have to use some kind of reflection:

  • either runtime compilation
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(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] =
  tb.eval(q"_root_.scala.Predef.implicitly[Operation[${cm.moduleSymbol(enumType.getClass)}]]")
    .asInstanceOf[Operation[EnumType]]
  • or a macro
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(s => {
    val module = s.asClass.module
    cq"${pq"`$module`"} => _root_.scala.Predef.implicitly[Operation[$module.type]]"
  })
  q"$enumType match { case ..$cases }"
}

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

  • Related