You are right to need more information about the type, and in general, if you have a value with HList as a static type, you probably have to change your approach. Essentially, you can't do anything with an HList if all you know is that it is an HList (other than the values ββthat precede it), and you usually only write HList as a type constraint.
In your case, what you are describing is a kind of sequence aligned in type. Before moving on with this approach, I suggest that you be sure what you really need. One of the nice things about functions (and types of functions like your Conversion ) is that they make up: you have A => B and B => C , and you create them in A => C and you can forget about B forever. You get a nice clean black box, which is usually exactly what you want.
In some cases, it may be helpful to be able to compose functionally similar things so that you can reflect on parts of the pipeline. I am going to assume that this is one of those cases, but you must confirm it yourself. If this is not so, you are lucky because what comes is random.
I will use these types:
trait Convertable trait Conversion[A <: Convertable, B <: Convertable] { def convert(a: A): B }
We can define a type class that indicates that a particular HList consists of one or more transformations whose types line up:
import shapeless._ trait TypeAligned[L <: HList] extends DepFn1[L] { type I <: Convertable type O <: Convertable type Out = Conversion[I, O] }
L contains all the type information about the pipeline, and I and O are the types of its endpoints.
Next, we need instances for this type (note that this must be defined together with the above attribute in order for these two to be related):
object TypeAligned { type Aux[L <: HList, A <: Convertable, B <: Convertable] = TypeAligned[L] { type I = A type O = B } implicit def firstTypeAligned[ A <: Convertable, B <: Convertable ]: TypeAligned.Aux[Conversion[A, B] :: HNil, A, B] = new TypeAligned[Conversion[A, B] :: HNil] { type I = A type O = B def apply(l: Conversion[A, B] :: HNil): Conversion[A, B] = l.head } implicit def composedTypeAligned[ A <: Convertable, B <: Convertable, C <: Convertable, T <: HList ](implicit tta: TypeAligned.Aux[T, B, C] ): TypeAligned.Aux[Conversion[A, B] :: T, A, C] = new TypeAligned[Conversion[A, B] :: T] { type I = A type O = C def apply(l: Conversion[A, B] :: T): Conversion[A, C] = new Conversion[A, C] { def convert(a: A): C = tta(l.tail).convert(l.head.convert(a)) } } }
And now you can write a version of your AutoConversion that tracks all the type information about the pipeline:
class AutoConversion[L <: HList, A <: Convertable, B <: Convertable]( path: L )(implicit ta: TypeAligned.Aux[L, A, B]) extends Conversion[A, B] { def convert(a: A): B = ta(path).convert(a) }
And you can use it as follows:
case class AutoA(i: Int) extends Convertable case class AutoB(s: String) extends Convertable case class AutoC(c: Char) extends Convertable val ab: Conversion[AutoA, AutoB] = new Conversion[AutoA, AutoB] { def convert(a: AutoA): AutoB = AutoB(aitoString) } val bc: Conversion[AutoB, AutoC] = new Conversion[AutoB, AutoC] { def convert(b: AutoB): AutoC = AutoC(bslift(3).getOrElse('-')) } val conv = new AutoConversion(ab :: bc :: HNil)
And conv will have the expected static type (and implement Conversion[AutoA, AutoC] ).