Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mapping hash map key/value pairs to named constructor arguments in Scala

Tags:

scala

Is it possible to map the key value pairs of a Map to a Scala constructor with named parameters?

That is, given

class Person(val firstname: String, val lastname: String) {
    ...
}

... how can I create an instance of Person using a map like

val args = Map("firstname" -> "John", "lastname" -> "Doe", "ignored" -> "value")

What I am trying to achieve in the end is a nice way of mapping Node4J Node objects to Scala value objects.

like image 616
Christoffer Soop Avatar asked Sep 17 '11 15:09

Christoffer Soop


3 Answers

The key insight here is that the constructor arguments names are available, as they are the names of the fields created by the constructor. So provided that the constructor does nothing with its arguments but assign them to fields, then we can ignore it and work with the fields directly.

We can use:

def setFields[A](o : A, values: Map[String, Any]): A = {
  for ((name, value) <- values) setField(o, name, value)
  o
}

def setField(o: Any, fieldName: String, fieldValue: Any) {
  // TODO - look up the class hierarchy for superclass fields
  o.getClass.getDeclaredFields.find( _.getName == fieldName) match {
    case Some(field) => {
      field.setAccessible(true)
      field.set(o, fieldValue)
    }
    case None =>
      throw new IllegalArgumentException("No field named " + fieldName)
  }

Which we can call on a blank person:

test("test setFields") {
  val p = setFields(new Person(null, null, -1), Map("firstname" -> "Duncan", "lastname" -> "McGregor", "age" -> 44))
  p.firstname should be ("Duncan")
  p.lastname should be ("McGregor")
  p.age should be (44)
}

Of course we can do better with a little pimping:

implicit def any2WithFields[A](o: A) = new AnyRef {
  def withFields(values: Map[String, Any]): A = setFields(o, values)
  def withFields(values: Pair[String, Any]*): A = withFields(Map(values :_*))
}

so that you can call:

new Person(null, null, -1).withFields("firstname" -> "Duncan", "lastname" -> "McGregor", "age" -> 44)

If having to call the constructor is annoying, Objenesis lets you ignore the lack of a no-arg constructor:

val objensis = new ObjenesisStd 

def create[A](implicit m: scala.reflect.Manifest[A]): A = 
  objensis.newInstance(m.erasure).asInstanceOf[A]

Now we can combine the two to write

create[Person].withFields("firstname" -> "Duncan", "lastname" -> "McGregor", "age" -> 44)
like image 58
Duncan McGregor Avatar answered Nov 16 '22 08:11

Duncan McGregor


You mentioned in the comments that you're looking for a reflection based solution. Have a look at JSON libraries with extractors, which do something similar. For example, lift-json has some examples,

case class Child(name: String, age: Int, birthdate: Option[java.util.Date])

val json = parse("""{ "name": null, "age": 5, "birthdate": null }""")
json.extract[Child] == Child(null, 5, None)

To get what you want, you could convert your Map[String, String] into JSON format and then run the case class extractor. Or you could look into how the JSON libraries are implemented using reflection.

like image 24
Kipton Barros Avatar answered Nov 16 '22 07:11

Kipton Barros


I guess you have domain classes of different arity, so here it is my advice. (all the following is ready for REPL)

Define an extractor class per TupleN, e.g. for Tuple2 (your example):

class E2(val t: Tuple2[String, String]) {
  def unapply(m: Map[String,String]): Option[Tuple2[String, String]] =
    for {v1 <- m.get(t._1)
         v2 <- m.get(t._2)}
    yield (v1, v2)
}

// class E3(val t: Tuple2[String,String,String]) ...

You may define a helper function to make building extractors easier:

def mkMapExtractor(k1: String, k2: String) = new E2( (k1, k2) )
// def mkMapExtractor(k1: String, k2: String, k3: String) = new E3( (k1, k2, k3) )

Let's make an extractor object

val PersonExt = mkMapExtractor("firstname", "lastname")

and build Person:

val testMap = Map("lastname" -> "L", "firstname" -> "F")
PersonExt.unapply(testMap) map {Person.tupled}

or

testMap match {
  case PersonExt(f,l) => println(Person(f,l))
  case _ => println("err")
}

Adapt to your taste.

P.S. Oops, I didn't realize you asked about named arguments specifically. While my answer is about positional arguments, I shall still leave it here just in case it could be of some help.

like image 1
Alexander Azarov Avatar answered Nov 16 '22 07:11

Alexander Azarov