Instantiate case class from List[_] of values

Hello, newbie question here, somewhat similar to this thread and also this one, but for Scala 2.

I have a data record coming from a Java library, in the form of a List[Object]. I know in advance the size and the sequence of types in the list, so it would make sense to create a case class to hold the data, in order to facilitate field access in the downstream logic.

I’d like to create a generic utility function, that takes in two parameters:

  • a reference to the case class’ generated companion object (aka its factory function)
  • the list of values to populate my instance with

The function should return an instance of the case class, or throw an exception in case the List elements don’t match the case class types.

I’ve found a way to accomplish this by calling the .curried method exposed by the generated companion object of the case class, using recursion to pierce through the chain of functions, while passing in my list values along the way:

def rec[T,R](cur: T => _, vs: List[_]): R = {
  cur(vs.head.asInstanceOf[T]) match {
    case f: Function1[_, _] => rec(f, vs.tail)
    case o => o.asInstanceOf[R] // reached the end of the function chain
  }
}

def fill[T,R](cc: Function1[T,R], values: List[_]): R = rec[T,R](cc, values) // no recursion in this case
def fill[T,R](cc: Function2[T,_,R], values: List[_]): R = rec[T,R](cc.curried, values)
def fill[T,R](cc: Function3[T,_,_,R], values: List[_]): R = rec[T,R](cc.curried, values)
...
def fill[T,R](cc: Function22[T,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,R], values: List[_]): R = rec[T,R](cc.curried, values)

Example usage:

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

scala> fill(MyCaseClass, List(42, "foo", 4.2))
val res1: MyCaseClass = MyCaseClass(42,foo,4.2)

This works, but unfortunately i couldn’t find another way to get a hold of R (the type of the case class, aka the return type of the factory function), other than enumerating all the possible overloads Function1 … Function22

What do you think about this solution?

  • is this what .curried was intended for ?
  • can the overloads (Function1…Function22) be avoided somehow ?
  • if the overloads can’t be avoided, should i simply call the .apply method directly for each possible arity and not worry about the code bloat? As in:
def fill[T,R](cc: Function3[T1,T2,T3,R], vs: List[_]): R = cc.apply(
  vs(0).asInstanceOf[T1],
  vs(1).asInstanceOf[T2],
  vs(2).asInstanceOf[T3])
  • or, should i just use reflection and iterate on the .apply method
    parameters? The overloads would still be needed to statically enforce the
    return type, though…

Sorry for the dumb questions, i don’t have any formal CS education and my usage of Scala is pretty basic level. I’m curious about what more experienced Scala devs would do in this case.

Any suggestion or opinion is appreciated :slight_smile:

In general, the first step would be to try to avoid the List[Any] in the first place… but since you said this comes from a Java library, it probably is not feasible to do that.

Other than that, you have a very common problem, map some data from an external source into a correctly typed representation in the host language.
There are multiple approaches to solving this problem, yet the most common one in Scala would be to write a type class that represents the “decoding” logic, and use Shapeless to derive instances for case classes.

Thus, you would have something like

trait Decoder[A] {
  def decode(data: List[Any]): Either[Error, A] // Error may be just String.
}

Then, you provide instances for the primitive types and then use Shapeless to derive a Decoder[SomeCaseClass].
Having said that, such an approach requires a lot of code which makes sense for a public library but not so much for a small private project.

Since, in your case, the decoding logic is a simple cast, I would rather have the overloads as you did.
I probably would write a code generator task for sbt, to generate all the overloads and not use curry to avoid the code repetition, but I know folks that would just copy and paste and forget about it.

My final advice would be to use Either instead of throw… but that is also mostly style.

1 Like

Thanks for your quick reply! Glad to hear i wasn’t too much off-route after all :slight_smile:

I’ll have to dive into Shapeless sooner or later (once i’ve overcome the fear of all the maths involved :rofl:), anyway my use case here is really trivial.
So, i think i’ll go with the code-generated overloads for now.

Thanks again for your expertise.

With Shapeless

import shapeless.{Generic, HList}
import shapeless.ops.function.FnToProduct
import shapeless.ops.traversable.FromTraversable

// cc is actually applied
def fill[F, L <: HList, R](cc: F, values: List[_])(implicit
  fnToProduct: FnToProduct.Aux[F, L => R],
  fromTraversable: FromTraversable[L]
): R = fnToProduct(cc)(fromTraversable(values).get)

or

// cc is used only to infer types, constructor is used
def fill[F, L <: HList, R](cc: F, values: List[_])(implicit
  fnToProduct: FnToProduct.Aux[F, L => R],
  generic: Generic.Aux[R, L],
  fromTraversable: FromTraversable[L]
): R = generic.from(fromTraversable(values).get)

fill(MyCaseClass.apply _, List(42, "foo", 4.2)) // MyCaseClass(42,foo,4.2)

or

// no cc, R is specified as a type
def fill[R] = new PartiallyAppliedFill[R]

class PartiallyAppliedFill[R] {
  def apply[L <: HList](values: List[_])(implicit
    generic: Generic.Aux[R, L],
    fromTraversable: FromTraversable[L]
  ): R = generic.from(fromTraversable(values).get)
}

fill[MyCaseClass](List(42, "foo", 4.2)) // MyCaseClass(42,foo,4.2)

You can avoid Option.get but then the return type will be Option[R] or Either[..., R] rather than R.

1 Like