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.
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)
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With