mdedetrich
mdedetrich

Reputation: 1889

Using Shapeless Polyfil to generically map over record

Akin to this question here (Mapping over Shapeless record), I am attempting to map over a trivial shapeless record (in this case, if I encounter a value of type Int, I want to convert it to a Double).

object Main extends App {
  import shapeless._ ; import syntax.singleton._ ; import record._
  import ops.record._
  import syntax.singleton._

  case class S(int:Int,t:String)

  val s = S(3,"a")

  val gen = LabelledGeneric[S]

  val rec = gen.to(s)

  val extended = rec + ('inPrint ->> true)

  val removed = rec - 'int

  val keys = Keys[gen.Repr]

  val options = 
    ('awesomeString ->> "a") ::
    ('epicInt ->> 5:Int) ::
    HNil

  def intToDouble(i:Int):Double = i.toDouble

  object bind extends FieldPoly {
    implicit def rpb[T, K](implicit witness: Witness.Aux[K]): Case.Aux[
      FieldType[K, Int],
      FieldType[K, Double]
      ] = atField(witness)(intToDouble)
  }

  val z = options.map(bind)

}

When I try to compile however, I get the following error

could not find implicit value for parameter mapper: shapeless.ops.hlist.Mapper[Main.bind.type,shapeless.::[String with shapeless.record.KeyTag[Symbol with shapeless.tag.Tagged[String("awesomeString")],String],shapeless.::[Int,shapeless.HNil]]]

Is there something critical that I am missing?

Upvotes: 2

Views: 457

Answers (1)

Miles Sabin
Miles Sabin

Reputation: 23046

There's one very minor problem with your example. You have,

val options = 
  ('awesomeString ->> "a") ::
  ('epicInt ->> 5:Int) ::
  HNil

If you paste this into the REPL you'll see that you've lost track of the 'epicInt key in the last element of the record. This is because the type ascription binds less tightly than the ->> operator so you have, in effect, first tagged your value with its key and then immediately thrown it away again. This leaves you with a valid HList, but unfortunately not one which is the right shape to be a record. The fix is to either drop the type ascription altogether, or use parentheses (ie. (5: Int).

The bigger problem is your use of FieldPoly here. FieldPoly is intended for situations which the key you want to operate on is statically known. That's not the case here: the type variable K is free and would have to be inferred. Unfortunately there's no way that it can be: it would have to be inferred from the first argument to atField but that, in turn, depends on K via the implicit definition of witness.

It could be that a different variant of FieldPoly that better matches your scenario would be useful. In the meantime, an ordinary Poly1 which operates on whole fields (ie. the key combined with the value) will do what you want,

trait bind0 extends Poly1 {
  implicit def default[E] = at[E](identity) // default case for non-Int fields
}

object bind extends bind0 {
  implicit def caseInt[K] =                 // case for fields with Int values 
    at[FieldType[K, Int]](field[K](intToDouble _))
}

Upvotes: 2

Related Questions