Search code examples
scalatypeclassshapeless

When trying to convert a Map to scala objects using Shapeless, Coproduct derivatives are not picked up the compiler


I am trying to convert Map[String, AttributeValue] to Scala objects. AttributeValue represents data in DynamoDB. FromAttributeValue in the below stub is used to convert basic cases

Following is code stub:

trait FromMap[L] {
  def apply(m: Map[String, AttributeValue]): Option[L]
}

object FromMap extends LowerPriorityFromMapInstances {

  implicit val hnilFromMap: FromMap[HNil] = ???

  implicit def hconsFromMap0[K <: Symbol, V, T <: HList](implicit
      witness: => Witness.Aux[K],
      fromAttributeValue: => Lazy[FromAttributeValue[V]],
      fromMapT: => FromMap[T],
  ): FromMap[FieldType[K, V] :: T] = ???

  implicit def cnilFromMap: FromMap[CNil] = ???

  implicit def coproductFromMap[K <: Symbol, H, T <: Coproduct](implicit 
      wit: => Witness.Aux[K],
      fromMapH: => Lazy[FromMap[H]],
      fromMapT: => FromMap[T]
  ): FromMap[FieldType[K, H] :+: T] = ???
}

trait LowerPriorityFromMapInstances {

//Other instances as required


  implicit def hConsFromMap1[K <: Symbol, V, R <: HList, T <: HList](implicit
      wit: => Witness.Aux[K],
      gen: => LabelledGeneric.Aux[V, R],
      fromMapH: => Lazy[FromMap[R]],
      fromMapT: => FromMap[T]
  ): FromMap[FieldType[K, V] :: T] = ???
 
}

Following is a scastie for the same.

Compiler is able to find derivations for all cases except for sealed traits. How can I make this work?


Solution

  • Firstly, since your representation types are now not only HLists, but also Coproducts, you should remove upper bounds <: HList in these two places

    implicit class FromMapOps[L /*<: HList*/](val a: Map[String, AttributeValue]) extends AnyVal { ...
    //                            ^^^^^^^^
    
    implicit def hConsFromMap1[K <: Symbol, V, R /*<: HList*/, T <: HList](implicit ...
    //                                             ^^^^^^^^
    

    Secondly, in implicit class FromMapOps you're mixing recursion logic with definition of extension method, so you don't actually define an instance of type class FromMap for a case class (and sealed trait). But if you start to resolve Map.empty[String, AttributeValue].toObj[ContainerUser] then you'll see that you need such instance, namely FromMap for GroupContainer

    Map.empty[String, AttributeValue].toObj[ContainerUser]
    
    implicitly[FromMap[FieldType[Witness.`'GroupContainer`.T, GroupContainer] :+: CNil]](
        FromMap.coproductFromMap(
          implicitly[Witness.Aux[Witness.`'GroupContainer`.T]],
          Lazy(implicitly[FromMap[GroupContainer]]),
          implicitly[FromMap[CNil]]
        )
      )
    
    implicitly[FromMap[GroupContainer]]
    

    So better don't mix recursion logic with logic defining extension method, keep logic defining instances of a type class and logic defining extension method separate. Try to define an instance of type class

    implicit def genericFromMap[A, L](implicit
                                          gen: => LabelledGeneric.Aux[A, L],
                                          fromMap: => FromMap[L],
                                     ): FromMap[A] = ???
    

    and an extension method

    implicit class FromMapOps[L](val a: Map[String, AttributeValue]) extends AnyVal {
      def toObj[A](implicit
                    fromMap: FromMap[A]
                  ): Option[A] = fromMap(a)
    }
    

    https://scastie.scala-lang.org/DmytroMitin/JwhJuXiXRBOT08RjZyywKg/2 (NotImplementedError means that the code compiles.)

    You can find several SO questions about converting between a case class and map (search for case class to map scala, map to case class scala).