Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I transform a Map to a case class in Scala?

If I have a Map[String,String]("url" -> "xxx", "title" -> "yyy"), is there an way to generically transform it into a case class Image(url:String, title:String)?

I can write a helper:

object Image{
  def fromMap(params:Map[String,String]) = Image(url=params("url"), title=params("title"))
}

but is there a way to generically write this once for a map to any case class?

like image 880
tommy chheng Avatar asked May 31 '11 00:05

tommy chheng


People also ask

For which kind of data should you use a case class in Scala?

A Scala Case Class is like a regular class, except it is good for modeling immutable data. It also serves useful in pattern matching, such a class has a default apply() method which handles object construction. A scala case class also has all vals, which means they are immutable.

What is the difference between class and case class in Scala?

A class can extend another class, whereas a case class can not extend another case class (because it would not be possible to correctly implement their equality).

Why do we use case class in Scala?

The one of the topmost benefit of Case Class is that Scala Compiler affix a method with the name of the class having identical number of parameters as defined in the class definition, because of that you can create objects of the Case Class even in the absence of the keyword new.


2 Answers

First off, there are some safe alternatives you could do if you just want to shorten your code. The companion object can be treated as a function so you could use something like this:

def build2[A,B,C](m: Map[A,B], f: (B,B) => C)(k1: A, k2: A): Option[C] = for {
  v1 <- m.get(k1)
  v2 <- m.get(k2)
} yield f(v1, v2)

build2(m, Image)("url", "title")

This will return an option containing the result. Alternatively you could use the ApplicativeBuilders in Scalaz which internally do almost the same but with a nicer syntax:

import scalaz._, Scalaz._
(m.get("url") |@| m.get("title"))(Image)

If you really need to do this via reflection then the easiest way would be to use Paranamer (as the Lift-Framework does). Paranamer can restore the parameter names by inspecting the bytecode so there is a performance hit and it will not work in all environments due to classloader issues (the REPL for example). If you restrict yourself to classes with only String constructor parameters then you could do it like this:

val pn = new CachingParanamer(new BytecodeReadingParanamer)

def fill[T](m: Map[String,String])(implicit mf: ClassManifest[T]) = for {
  ctor <- mf.erasure.getDeclaredConstructors.filter(m => m.getParameterTypes.forall(classOf[String]==)).headOption
  parameters = pn.lookupParameterNames(ctor)
} yield ctor.newInstance(parameters.map(m): _*).asInstanceOf[T]

val img = fill[Image](m)

(Note that this example can pick a default constructor as it does not check for the parameter count which you would want to do)

like image 111
Moritz Avatar answered Nov 15 '22 20:11

Moritz


Here's a solution using builtin scala/java reflection:

  def createCaseClass[T](vals : Map[String, Object])(implicit cmf : ClassManifest[T]) = {
      val ctor = cmf.erasure.getConstructors().head
      val args = cmf.erasure.getDeclaredFields().map( f => vals(f.getName) )
      ctor.newInstance(args : _*).asInstanceOf[T]
  }

To use it:

val image = createCaseClass[Image](Map("url" -> "xxx", "title" -> "yyy"))
like image 31
Andrejs Avatar answered Nov 15 '22 20:11

Andrejs