Instantiating a case class from a list of parameters
Asked Answered
N

4

46

Given:

case class Foo(a: Int, b: String, c: Double)

you can say:

val params = Foo(1, "bar", 3.14).productIterator.toList

and get:

params: List[Any] = List(1, bar, 3.14)

Is there a way to "go backwards" and recreate a Foo object directly from this list, i.e.:

Foo.createFromList(params)   // hypothetical

instead of writing:

Foo(params(0).asInstanceOf[Int], params(1).asInstanceOf[String], params(2).asInstanceOf[Double])

EDIT: it seems that it boils down to being able to send the elements of a list as parameters to a function without writing them out explicitly, e.g.:

def bar(a: Int, b: Int, c: Int) = //...
val list = List(1, 2, 3, 4, 5)
bar(list.take(3)) // hypothetical, instead of:
bar(list(0), list(1), list(2))

I would sort of expect to be able to do:

bar(list.take(3): _*)

but that doesn't seem to work.

EDIT: Solution based on extempore's answer, but invoking the constructor directly instead of using the apply method:

case class Foo(a: Int = 0, b: String = "bar", c: Double = 3.14) {
    val cs = this.getClass.getConstructors
    def createFromList(params: List[Any]) =
    cs(0).newInstance(params map { _.asInstanceOf[AnyRef] } : _*).asInstanceOf[Foo]
}

Now you can do:

scala> Foo().createFromList(List(4, "foo", 9.81))
res13: Foo = Foo(4,foo,9.81)

You can also refactor the creation method into a trait:

trait Creatable[T <: Creatable[T]] {
    val cs = this.getClass.getConstructors
    def createFromList(params: List[Any]) =
        cs(0).newInstance(params map { _.asInstanceOf[AnyRef] } : _*).asInstanceOf[T]   
}

case class Bar(a: Int = 0, b: String = "bar", c: Double = 3.14) extends Creatable[Bar]

And do e.g.:

scala> val bar = Bar()
bar: Bar = Bar(0,bar,3.14)

scala> bar == bar.createFromList(bar.productIterator.toList)
res11: Boolean = true
Narcose answered 27/11, 2010 at 9:33 Comment(1)
doing some fast benchmarking you can find that just pasting manually all the values, e.g. Foo(list(0), list(1), list(2)) it's x10 faster. Testing on a case class which has 43 values.Rupp
M
57
scala> case class Foo(a: Int, b: String, c: Double)
defined class Foo

scala> val params = Foo(1, "bar", 3.14).productIterator.toList
params: List[Any] = List(1, bar, 3.14)

scala> Foo.getClass.getMethods.find(x => x.getName == "apply" && x.isBridge).get.invoke(Foo, params map (_.asInstanceOf[AnyRef]): _*).asInstanceOf[Foo]
res0: Foo = Foo(1,bar,3.14)

scala> Foo(1, "bar", 3.14) == res0
res1: Boolean = true

Edit: by the way, the syntax so far only being danced around for supplying the tuple as an argument is:

scala> case class Foo(a: Int, b: String, c: Double)
defined class Foo

scala> Foo.tupled((1, "bar", 3.14))                
res0: Foo = Foo(1,bar,3.14)
Marylouisemaryly answered 28/11, 2010 at 5:15 Comment(3)
That seems to be an actual solution. How much overhead does the use of reflection create? (And what is the significance of the isBridge check?)Narcose
isBridge is just a cheap way to pick the right method. There's another one with the same name which takes (Int, String, Double) instead of (AnyRef, AnyRef, AnyRef) and that call might not go so well. I can't answer the overhead question in any meaningful way. "Some."Marylouisemaryly
You can also invoke the constructor directly. I've edited my question with an example of this.Narcose
I
15

You could use pattern matching like:

params match {                                   
 case List(x:Int, y:String, d:Double) => Foo(x,y,d)
}
Ivaivah answered 27/11, 2010 at 11:23 Comment(0)
R
15

Well, you can certainly do this with a tuple:

(Foo _).tupled apply (1, bar, 3.14)

But there is no real way to get from a List[S] to (A, B, C) for A, B, C <: S. There may be a way of doing this with HLists of course

Regardful answered 27/11, 2010 at 12:20 Comment(4)
That won't actually work! The Foo companion singleton isn't actually a Function, it's just a class with an apply method. (Foo.apply _).tupled should do the trick though.Revocation
It's not? "public final class Foo$ extends scala.runtime.AbstractFunction3 implements scala.ScalaObject,java.io.Serializable"Marylouisemaryly
@Kevin - thanks, I am pretty sure that I have not had to declare the apply explicitly in my own codeRegardful
@KevinWright it is when there's no type parameters involved: #25345711Racketeer
F
1

Another one liner using case class companion object curried method and completely ignoring type safety :)

scala> case class Foo(a: Int, b: String, c: Double)
defined class Foo

scala> val lst = List(1, "bar", 3.14)
lst: List[Any] = List(1, bar, 3.14)

scala> val foo = lst.foldLeft(Foo.curried: Any){case (r, v) => r.asInstanceOf[Function[Any, _]](v) }.asInstanceOf[Foo]
foo: Foo = Foo(1,bar,3.14)
Ferret answered 18/5, 2020 at 22:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.