Extracting a value of a given type from a case class

940 Views Asked by At

Is it possible, using Shapeless, to extract a value of a specific type from a case class? So far, I can do this:

def fromCaseClass[T, R <: HList](value: T)(implicit ga: Generic.Aux[T, R]): R = {
  ga.to(value)
}

Which then allows me to extract values procedurally:

scala> case class ServiceConfig(host: String, port: Int, secure: Boolean)
defined class ServiceConfig

scala> val instance = ServiceConfig("host", 80, true)
instance: ServiceConfig = ServiceConfig(host,80,true)

scala> fromCaseClass(instance).select[Boolean]
res10: Boolean = true

scala> fromCaseClass(instance).select[Int]
res11: Int = 80

However, when I try to write a function to do this, I'm getting tied up with implicits not found:

def getByType[C, X](value: C)(implicit ga: Generic.Aux[C, X]): X = {
  fromCaseClass(value).select[X]
}

<console>:12: error: could not find implicit value for parameter ga: shapeless.Generic.Aux[C,R]
       fromCaseClass(value).select[X]

Presumably I'm getting this because the compiler can't verify that my parameter is not a case class. Is there a way for me to do this?

I'm quite new to Shapeless, so I'm not entirely sure if I'm trying to do something crazy or if missing something simple.

Update

I feel I'm getting a bit closer. I can implement as so:

def newGetByType[C, H <: HList, R]
     (value: C)
     (implicit ga: Generic.Aux[C, H], selector: Selector[H, R]): R = {
  ga.to(value).select[R]
}

And this allows me to select from a case class:

scala> val s: String = newGetByType(instance)
s: String = host

But this only seems to work for the first type in the case class:

scala> val i: Int = newGetByType(instance)
<console>:17: error: type mismatch;
 found   : String
 required: Int
       val i: Int = newGetByType(instance)

Am I on the right track?

2

There are 2 best solutions below

1
On BEST ANSWER

You were getting close ...

The main problem with your newGetByType is that it doesn't give you any way to explicitly specify the type you're hoping to extract (and, as you've observed, typing the val on the LHS isn't sufficient to allow it to be inferred).

Why can't you explicitly specify the type to be extracted? Here's the definition again,

def getByType[S, C, L <: HList](value: C)
  (implicit gen: Generic.Aux[C, L], sel: Selector[L, S]): S =
    gen.to(value).select[S]

Ideally we'd like to be able to specify the type argument S, allow C to be inferred from the value argument, and L to be computed by implicit resolution from C. Unfortunately, however, Scala doesn't allow us to partially specify type arguments ... it's all or nothing.

So the trick to get this to work is to split the type parameter list into two: one which can be fully specified explicitly and one which can be fully inferred: this is a general technique, not one which is specific to shapeless.

We do this by moving the main part of the computation to an auxiliary class which is parametrized by the type we are going to supply explicitly,

class GetByType[S] {
  def apply[C, L <: HList](value: C)
    (implicit gen: Generic.Aux[C, L], sel: Selector[L, S]): S =
      gen.to(value).select[S]
}

Notice that here we can now assume that S is known, and both of the type arguments to apply can be inferred. We finish up with the now trivial definition of getByType which just provides a place where the explicit type argument can go, and instantiates the auxiliary class,

def getByType[S] = new GetByType[S]

This gives you the result you want,

scala> import shapeless._, ops.hlist._
import ops.hlist._

scala> :paste
// Entering paste mode (ctrl-D to finish)

class GetByType[S] {
  def apply[C, L <: HList](value: C)
    (implicit gen: Generic.Aux[C, L], sel: Selector[L, S]): S =
      gen.to(value).select[S]
}

// Exiting paste mode, now interpreting.

defined class GetByType

scala> def getByType[S] = new GetByType[S]
getByType: [S]=> GetByType[S]

scala> case class ServiceConfig(host: String, port: Int, secure: Boolean)
defined class ServiceConfig

scala> val instance = ServiceConfig("host", 80, true)
instance: ServiceConfig = ServiceConfig(host,80,true)

scala> getByType[String](instance)
res0: String = host

scala> getByType[Int](instance)
res1: Int = 80

scala> getByType[Boolean](instance)
res2: Boolean = true
3
On

I guess you have to pass ga explicitly with the call, because otherwise it will not be in scope for the called function (i.e. fromCaseClass):

def getByType[C, X](value: C)(implicit ga: Generic.Aux[C, X]): X = {
  fromCaseClass(value)(ga).select[X]
}

Other way would be to bring this parameter into scope before fromCaseClass is called:

def getByType[C, X](value: C)(implicit ga: Generic.Aux[C, X]): X = {
    val innerGa = ga
    fromCaseClass(value).select[X]
}