Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to do generic tuple -> case class conversion in Scala?

Tags:

scala

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?
like image 461
Roy Avatar asked Jun 09 '15 09:06

Roy


1 Answers

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)
  }
}
like image 102
Régis Jean-Gilles Avatar answered Oct 10 '22 23:10

Régis Jean-Gilles