shapeless Mapper for LabelledGeneric not found

686 views Asked by At

I have basic type pool defined like that:

sealed trait Section
final case class Header(...) extends Section
final case class Customer(...) extends Section
final case class Supplier(...) extends Section
final case class Tech(...) extends Section

I'd like to present some case classes composed of types from this pool like this:

final case class ContractViewPartners(customer: Customer, supplier: Supplier)
final case class ContractView(header: Header, partners: ContractViewPartners, tech: Tech)

As they would be heavily used in feature-generators implemented via transfoming to HLists using method described here, I'd like to ensure that each field of presented type is one of

  • Section subtype
  • HList of Section subtypes
  • record presentable as HList of Section subtypes

I've defined simple compile-time checker for this condition:

object traverseView extends Poly1 {
  implicit def caseSection[S <: Section] = at[S](_ => ())

  implicit def caseSectionList[L <: HList]
  (implicit evt: ToTraversable.Aux[L, List, Section]) = at[L](_ => ())

  implicit def caseRecord[R, L <: HList]
  (implicit lgen: LabelledGeneric.Aux[R, L],
   trav: ToTraversable.Aux[L, List, Section]) = at[R](_ => ())
}

private def contractViewIsMultiSection(v: ContractView) =  {
  val gen = LabelledGeneric[ContractView].to(v)
  gen map traverseView
}

But it fails with (package names removed)

could not find implicit value for parameter mapper: Mapper[traverseView.type,::[Header with KeyTag[Symbol with Tagged[String("header")],Header],::[ContractViewPartners with KeyTag[Symbol with Tagged[String("partners")],ContractViewPartners],::[Tech with KeyTag[Symbol with Tagged[String("tech")],Tech],HNil]]]]

If i remove partners section from ContractView it's working and if i try to resolve implicits on ContractViewPartners they will be found too.

Again while writing question i've found solution with adding .values like that

private def contractViewIsMultiSection(v: ContractView) =  {
  val gen = LabelledGeneric[ContractView].to(v)
    .values //!!!
  gen map traverseView
}

Could it be that type with KeyTag[...] is not working properly as source for LabelledGeneric transformation?

1

There are 1 answers

0
Travis Brown On BEST ANSWER

The problem is that Case is invariant, so the fact that you have a Case instance for ContractViewPartners doesn't mean that you have a case instance for ContractViewPartners with a type-level label (which is only a subtype of ContractViewPartners). You can fix this pretty straightforwardly by generating instances for e.g. FieldType[K, ContractViewPartners] (for some arbitrary K):

sealed trait Section
final case class Header(s: String) extends Section
final case class Customer(s: String) extends Section
final case class Supplier(s: String) extends Section
final case class Tech(s: String) extends Section

final case class ContractViewPartners(customer: Customer, supplier: Supplier)
final case class ContractView(header: Header, partners: ContractViewPartners, tech: Tech)

import shapeless._, labelled.FieldType, ops.hlist.ToList

object traverseView extends Poly1 {
  implicit def caseSection[S <: Section] = at[S](_ => ())

  implicit def caseSectionList[K, L <: HList](implicit
    lub: ToList[L, Section] 
  ) = at[FieldType[K, L]](_ => ())

  implicit def caseRecord[K, C, L <: HList](implicit
    gen: Generic.Aux[C, L],
    lub: ToList[L, Section] 
  ) = at[FieldType[K, C]](_ => ())
}

private def contractViewIsMultiSection(v: ContractView) =  {
  val gen = LabelledGeneric[ContractView].to(v)
  gen map traverseView
}

You could also just use Generic[ContractView] in contractViewIsMultiSection if you don't care about the labels.

I would probably suggest not using Poly1 for this kind of thing, though. If you just want evidence that the types are right, you could do that a little more cleanly with a custom type class.