5

Let's say someone provided a function:

def getTupleData[T](source: String): List[T] = {
  // ...
}

I need to write a function which takes a case class C as the type parameter and return List[C] with the help of the above function. Here is what I have got so far:

def getCaseClassData[C](source: String): List[C] = {
  // Somehow get T from C.
  // For example, if C is case class MyCaseClass(a: Int, b: Long), then T is (Int, Long)
  // How to get T?      

  getTupleData[T](source) map { tuple: T =>
    // Somehow convert tuple into a case class instance with the case class type parameter
    // C.tupled(tuple) ??  Type parameter cannot be used like this. :(
  }
}

More specifically, it seems to me I'm asking two questions here:

  1. How to explicitly obtain the type of the tuple from a type parameter which represents a case class so that it can be used as a type parameter?
  2. How to create a case class instance from a tuple instance without knowing the actual name of the case class but only a type parameter?
Roy
  • 880
  • 1
  • 12
  • 27
  • 1
    http://stackoverflow.com/questions/8087958/in-scala-is-there-an-easy-way-to-convert-a-case-class-into-a-tuple – Ashalynd Jun 09 '15 at 09:58
  • This doesn't answer my question: 1) I'm talking about generics here, totally different story as I see it, and 2) the question you referred to is converting from case class to tuple. – Roy Jun 09 '15 at 09:59
  • What is denoted by `T` type? – Odomontois Jun 09 '15 at 10:10
  • I intended it to be the type of the corresponding tuple. – Roy Jun 09 '15 at 10:11

1 Answers1

9

You won't find any reasonably simple or direct way to do it. If you' re ready for the more involved solutions, bear with me.

Every case class has an apply method in its companion object, which instantiates the class. By calling tupled on this method (after eta-expansion), you'll get a function that takes a tuple and creates the corresponding case class instance.

Now of course the problem is that the every case class's apply has a different signature. We can get around this by introducing a type class representing a case class factory, and provide instances of this type class through a macro (which will just delegate to the case class's apply method).

import scala.reflect.macros.whitebox.Context
import scala.language.experimental.macros

trait CaseClassFactory[C,T]{
  type Class = C
  type Tuple = T
  def apply(t: Tuple): C
}

object CaseClassFactory {
  implicit def factory1[C,T]: CaseClassFactory[C,T] = macro factoryImpl[C,T]
  implicit def factory2[C]: CaseClassFactory[C,_] = macro factoryImpl[C,Nothing]
  def apply[C,T]: CaseClassFactory[C,T] = macro factoryImpl[C,T]
  def apply[C]: CaseClassFactory[C,_] = macro factoryImpl[C,Nothing]

  def factoryImpl[C:c.WeakTypeTag,T:c.WeakTypeTag](c: Context) = {
    import c.universe._
    val C = weakTypeOf[C]
    val companion = C.typeSymbol.companion match {
      case NoSymbol => c.abort(c.enclosingPosition, s"Instance of $C has no companion object")
      case sym      => sym
    }
    val tupledTree = c.typecheck(q"""($companion.apply _).tupled""")
    val T = tupledTree.tpe match {
      case TypeRef(_, _, List(argTpe, _)) => argTpe
      case t => c.abort(c.enclosingPosition, s"Expecting type constructor (Function1) for $C.tupled, but got $t: ${t.getClass}, ${t.getClass.getInterfaces.mkString(",")}")
    }
    if (! (c.weakTypeOf[T] <:< T)) {
      c.abort(c.enclosingPosition, s"Incompatible tuple type ${c.weakTypeOf[T]}: not a sub type of $T")
    }
    q"""
    new CaseClassFactory[$C,$T] {
      private[this] val tupled = ($companion.apply _).tupled
      def apply(t: Tuple): $C = tupled(t)
    }
    """
  }
}

With it you can do something like this:

scala> case class Person(name: String, age: Long)
defined class Person

scala> val f = CaseClassFactory[Person]
f: CaseClassFactory[Person]{type Tuple = (String, Long)} = $anon$1@63adb42c

scala> val x: f.Tuple = ("aze", 123)
x: f.Tuple = (aze,123)

scala> implicitly[f.Tuple =:= (String, Long)]
res3: =:=[f.Tuple,(String, Long)] = <function1>

scala> f(("aze", 123))
res4: Person = Person(aze,123)

But more importantly, you can require an instance of CaseClassFactory as an implicit parameter, allowing to generically instantiate your case classes. You can then do something like:

scala> implicit class TupleToCaseClassOps[T](val t: T) extends AnyVal {
     |   def toCaseClass[C](implicit f: CaseClassFactory[C,T]): C = {
     |     f(t)
     |   }
     | }
defined class TupleToCaseClassOps

scala> case class Person(name: String, age: Long)
defined class Person

scala> ("john", 21).toCaseClass[Person]
res5: Person = Person(john,21)

Pretty neat. Armed with this type class, getCaseClassData then becomes:

def getCaseClassData[C](source: String)(implicit f: CaseClassFactory[C,_]): List[C] = {
  getTupleData[f.Tuple](source) map { tuple: f.Tuple =>
    f(tuple)
  }
}
Régis Jean-Gilles
  • 32,541
  • 5
  • 83
  • 97