2

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?

Community
  • 1
  • 1
mdedetrich
  • 1,899
  • 1
  • 18
  • 29

1 Answers1

2

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 _))
}
Miles Sabin
  • 23,015
  • 6
  • 61
  • 95
  • Thanks heaps, solved me problem. Didn't realise that doing something like `5:Int` would have such an effect, I suppose its a limitation of Scala? – mdedetrich Nov 05 '14 at 11:46
  • It's an artefact of the way that fields are encoded in shapeless records: the type of a field is the type of the value tagged with the singleton type of the key. This has lots of nice properties ... in particular it means that keys are completely phantom and the record has the exact same runtime footprint as the corresponding `HList` of only the values. It also means that field types are subtypes of their value types, hence a field can be "converted" to its value with a simple type ascription of the form you used above. In some scenarios this is fine, but clearly not in this one. – Miles Sabin Nov 05 '14 at 13:57