Search code examples
scalashapeless

Dynamically create extensible record in shapeless 2.0


I need to produce an extensible record given an HList of keys and a map of values, here's a MWE of what I'm trying to achieve (you can copy/paste this in any REPL with shapeless 2.0 available, in order to reproduce the issue)

import shapeless._; import syntax.singleton._; import record._

case class Foo[T](column: Symbol)

val cols = Foo[String]('column1) :: HNil

val values = Map("column1" -> "value1")

object toRecord extends Poly1 {
  implicit def Foo[T] = at[Foo[T]] { foo =>
    val k = foo.column.name
    val v = values.get(k)
    (k ->> v)
  }
}

val r = cols.map(toRecord)
// r: shapeless.::[Option[String] with shapeless.record.KeyTag[k.type,Option[String]] forSome { val k: String },shapeless.HNil] = Some(value1) :: HNil

val value = r("column1")
// error: No field String("column1") in record shapeless.::[Option[String] with shapeless.record.KeyTag[k.type,Option[String]] forSome { val k: String },shapeless.HNil]
   val value = r("column1")

If I try defining the record manually everything works as expected

 val q = ("column1" ->> Some("value1")) :: HNil
 // q: shapeless.::[Some[String] with shapeless.record.KeyTag[String("column1"),Some[String]],shapeless.HNil] = Some(value1) :: HNil

 q("column1")
 // Some[String] = Some(value1)

Clearly the difference is that in one case the KeyTag has type

KeyTag[String("column1"), Some[String]]

and in the (non-working) other

KeyTag[k.type,Option[String]] forSome { val k: String }

I sense the issue is with the string k not being statically known, but I have no clue on how to fix this. Generally speaking, is there a way of dynamically generating an extensible record from a list of keys?

I fear the answer is to use a macro, but I'd be glad if another solution existed.


Solution

  • This isn't too bad if you can change your Foo definition a bit to allow it to keep track of the singleton type of the column key (note that I've removed the unused T type parameter):

    import shapeless._; import syntax.singleton._; import record._
    
    case class Foo[K <: Symbol](column: Witness.Aux[K])
    
    val cols = Foo('column1) :: HNil
    val values = Map("column1" -> "value1")
    
    object toRecord extends Poly1 {
      implicit def atFoo[K <: Symbol] = at[Foo[K]] { foo =>
        field[K](values.get(foo.column.value.name))
      }
    }
    
    val r = cols.map(toRecord)
    

    And then:

    scala> val value = r('column1)
    value: Option[String] = Some(value1)
    

    Note that I've changed your string key ("column1") to a symbol, since that's what we've put into the record.