Mapping hash map key/value pairs to named constructor arguments in 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.
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.
Since Map
is essentially just a List
of tuples you can treat it as such.
scala> val person = args.toList match {
case List(("firstname", firstname), ("lastname", lastname), _) => new Person(firstname, lastname)
case _ => throw new Exception
}
person: Person = Person(John,Doe)
I made Person
a case class to have the toString
method generated for me.
精彩评论