0

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))
  }
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
Tomek L
  • 3
  • 2

2 Answers2

1

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?

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • 1
    Thank you Dmytro for the solution and all additional resources. I'll go with macro version as you suggest - it works as I expected. – Tomek L Oct 08 '22 at 20:20
0
 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))
Dima
  • 39,570
  • 6
  • 44
  • 70
  • I'm afraid your solution will not work in OP's settings. Can you see that OP writes: *"value of enumType is dynamic - it can be even decoded from some json document `val enumType: EnumType = Add50`"*? So the static type of `enumType` is just `EnumType`. Your solution will give "implicit not found" or "ambiguous implicits". – Dmytro Mitin Oct 07 '22 at 18:15
  • 1
    Dima, thank you for your answer. You were able to remove pattern matching because you changed the way how you call the function. You call it by providing explicit `Add50` type, however in my case I will have some `EnumType` which will be known only at runtime and that's why I included casting here: `val enumType: EnumType = Add50` – Tomek L Oct 07 '22 at 18:18
  • Oh, I see. Well, if you don't know the type at compile time, then you are stuck with pattern matching. Either that, or just add add `getOperation` to `EnumType` itself. But FWIW, I don't think you actually _need_ it: from the code perspective, there isn't much difference between `foo(Add50)` and `foo[Add50]`, except that the latter lets you do what you want and the former does not. – Dima Oct 07 '22 at 18:22
  • @Dima Both`foo(Add50)` and `foo[Add50]` would be ok but OP doesn't have either of them, just `foo(enumType)` for `enumType: EnumType`. – Dmytro Mitin Oct 07 '22 at 20:51
  • @DmytroMitin yes, but where does `enumType` come from? You either set it right there like `val enumType = Add50` or it comes as a parameter `def foo(enumType: ЕnumType)`. So, the point is that the first case is trivial, and the second can easily be changed to `def foo[T <: EnumType: Operation]` – Dima Oct 07 '22 at 20:56
  • @Dima *"yes, but where does `enumType` come from?"* If I understood OP correctly, he is parsing a string (json). *"the second can easily be changed to `def foo[T: EnumType]`"* I guess you meant `T <: EnumType`. Not easily if we don't have `T` statically. – Dmytro Mitin Oct 07 '22 at 20:59