scalatypeclassimplicitshapelesscontext-bound

Shapeless: Mapping an natural transformation over a KList


Is there a way to map a natural transformation (e.g. a Option ~> Either[String, *]) over a KList (e.g. a HList with a UnaryTCConstraint)? That would seem to be the natural thing to do with a KList.

Specifically I was trying to do the following:

object myNaturalTransformation extends (Option ~> Either[String, *]) {
  def apply[T](a: Option[T]): Either[String, T] = a.toRight("oh noe!")
}

def doStuff[KList <: HList: *->*[Option]#λ](klist: KList) = {
  klist.map(myNaturalTransformation)
}

I understand that the missing piece is the Mapper required to perform the .map and that Shapeless isn't able to generate one from myNaturalTransformations cases and the UnaryTCConstraint. Is it possible to obtain one some other way? Or is there another approach to map over a KList that I'm overlooking (apart from passing a Mapper to the doStuff-function)?

I was able to write my own version of UnaryTCConstraint that includes a

def mapper[G[_], HF <: ~>[TC, G]](hf: HF): Mapper[hf.type, L]

to explicitly generate a mapper for a given natural transformation. However I am curious if it's possible to do that with Shapeless' implementation of UnaryTCConstraint.


Solution

  • UnaryTCConstraint (*->*) is not for mapping, it's a constraint (common for HLists, Coproducts, case classes and sealed traits). For mapping there are type classes NatTRel, Mapped, Comapped, Mapper etc. (separate for HLists and Coproducts).

    Try both constraint and type class

    def doStuff[KList <: HList: *->*[Option]#λ, L <: HList](klist: KList)(implicit 
      natTRel: NatTRel[KList, Option, L, Either[String, *]]
    ): L = natTRel.map(myNaturalTransformation, klist)
    

    or just type class

    def doStuff[KList <: HList, L <: HList](klist: KList)(implicit 
      natTRel: NatTRel[KList, Option, L, Either[String, *]]
    ): L = natTRel.map(myNaturalTransformation, klist)
    

    or hiding type parameter L via PartiallyApplied pattern

    def doStuff[KList <: HList] = new PartiallyAppliedDoStuff[KList]
    
    class PartiallyAppliedDoStuff[KList <: HList] {
      def apply[L <: HList](klist: KList)(implicit 
        natTRel: NatTRel[KList, Option, L, Either[String, *]]
      ): L = natTRel.map(myNaturalTransformation, klist)
    }
    

    or hiding type parameter L via existential (but then return type is not precise)

    def doStuff[KList <: HList](klist: KList)(implicit 
      natTRel: NatTRel[KList, Option, _, Either[String, *]]
    ) = natTRel.map(myNaturalTransformation, klist)
    

    or using extension method

    implicit class NatTRelOps[KList <: HList](val klist: KList) extends AnyVal {
      def map[F[_], G[_], L <: HList](f: F ~> G)(implicit 
        natTRel: NatTRel[KList, F, L, G]
      ): L = natTRel.map(f, klist)
    } 
    
    def doStuff[KList <: HList, L <: HList](klist: KList)(implicit 
      natTRel: NatTRel[KList, Option, L, Either[String, *]]
    ): L = klist.map(myNaturalTransformation)
    

    Testing:

    doStuff(Option(1) :: Option("a") :: HNil)           // compiles
    //doStuff(Option(1) :: Option("a") :: true :: HNil) // doesn't compile