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 myNaturalTransformation
s 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
.
UnaryTCConstraint
(*->*
) is not for mapping, it's a constraint (common for HList
s, Coproduct
s, case classes and sealed traits). For mapping there are type classes NatTRel
, Mapped
, Comapped
, Mapper
etc. (separate for HList
s and Coproduct
s).
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