St. St. - 3 months ago 12
Scala Question

Map single type HList to HList of target types

I have trait-marker

trait TypedTrait {
type TYPE
}


and the realization

case class TypedString[U](value: String) extends TypedTrait {
type TYPE = U
}


And I want to map
HList
of
String
into
HList
of
TypedString
in accordance with
TypedString
's type parameters.

The simplest way is to create
convert
method (as described in Shapeless map HList depending on target types):

val list = "Hello" :: "world" :: HNil

val mapped: TypedString[Int] :: TypedString[Boolean] :: HNil =
convert[TypedString[Int] :: TypedString[Boolean] :: HNil](list)


But I'd like to avoid redundant parameterization and use something like this:

val mapped: TypedString[Int] :: TypedString[Boolean] :: HNil =
convert[Int :: Boolean :: HNil](list)


Complete code example for the first solution:

import shapeless._

trait TypedTrait {
type TYPE
}

case class TypedString[U](value: String) extends TypedTrait {
type TYPE = U
}

trait Convert[I <: HList, O <: HList] { def apply(i: I): O }

object Convert extends LowPriorityConvertInstances {
implicit val convertHNil: Convert[HNil, HNil] = new Convert[HNil, HNil] {
def apply(i: HNil): HNil = i
}

implicit def convertHConsTS[TS, T <: HList, TO <: HList](implicit
c: Convert[T, TO]
): Convert[String :: T, TypedString[TS] :: TO] =
new Convert[String :: T, TypedString[TS] :: TO] {
def apply(i: String :: T): TypedString[TS] :: TO = TypedString[TS](i.head) :: c(i.tail)
}
}

sealed class LowPriorityConvertInstances {
implicit def convertHCons[H, T <: HList, TO <: HList](implicit
c: Convert[T, TO]
): Convert[H :: T, H :: TO] = new Convert[H :: T, H :: TO] {
def apply(i: H :: T): H :: TO = i.head :: c(i.tail)
}
}


class PartiallyAppliedConvert[O <: HList] {
def apply[I <: HList](i: I)(implicit c: Convert[I, O]): O = c(i)
}

def convert[O <: HList]: PartiallyAppliedConvert[O] =
new PartiallyAppliedConvert[O]

val list = "Hello" :: "world" :: HNil

val mapped: TypedString[Int] :: TypedString[String] :: HNil =
convert[TypedString[Int] :: TypedString[String] :: HNil](list)

Answer

You can achive this by having three HList type arguments in Convert:

  • the type of the actual HList passed to convert (e.g., String :: String :: HNil)
  • the type parameter prescribed by the user (e.g., Int :: Boolean :: HNil)
  • the output type – basically the prescribed HList wrapped in TypedString: e.g., TypedString[Int] :: TypedString[Boolean] :: HNil.

The output type can be completely calculated from the prescribed HList, so I'd use the Aux pattern commonly employed with shapeless code:

trait Convert[In <: HList, Prescribed <: HList] {
  type Out <: HList
  def apply(i: In): Out
}

object Convert {
  type Aux[I <: HList, P <: HList, O <: HList] = Convert[I, P] { type Out = O }

  // Adapt the implicits accordingly. 
  // The low priority one is left as an exercise to the reader.

  implicit val convertHNil: Convert.Aux[HNil, HNil, HNil] = 
    new Convert[HNil, HNil] {
      type Out = HNil
      def apply(i: HNil): HNil = i
    }

  implicit def convertHConsTS[TS, TI <: HList, TP <: HList, TO <: HList](implicit
    c: Convert.Aux[TI, TP, TO]
  ): Convert.Aux[String :: TI, TS :: TP, TypedString[TS] :: TO] =
    new Convert[String :: TI, TS :: TP] {
      type Out = TypedString[TS] :: TO
      def apply(i: String :: TI): TypedString[TS] :: TO = 
        TypedString[TS](i.head) :: c(i.tail)
    }
}  

class PartiallyAppliedConvert[P <: HList] {
  def apply[I <: HList](i: I)(implicit c: Convert[I, P]): c.Out = c(i)
}

def convert[O <: HList]: PartiallyAppliedConvert[O] =
  new PartiallyAppliedConvert[O]

val list = "Hello" :: "world" :: HNil

val mapped = convert[Int :: String :: HNil](list)

Result:

scala> mapped
res3: shapeless.::[com.Main.TypedString[Int],shapeless.::[com.Main.TypedString[String],shapeless.HNil]] = TypedString(Hello) :: TypedString(world) :: HNil

I believe it may be possible to achieve this using some operations provided with shapeless (shapeless.ops.hlist.Mapped, shapeless.ops.hlist.HKernel, or shapeless.ops.hlist.RightFolder look appropriate), but I don't know how to write a Poly function, that takes a type argument and a normal argument. Any tips would be welcome.