bit_drifter
bit_drifter

Reputation: 254

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.

Upvotes: 1

Views: 203

Answers (1)

Dmytro Mitin
Dmytro Mitin

Reputation: 51683

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

Upvotes: 2

Related Questions