Hai ragione sul bisogno di più informazioni sul tipo, e in generale se hai un valore con HList
come un tipo statico, è probabile che dovrai cambiare approccio. Non c'è praticamente nulla che tu possa fare con un HList
se tutto quello che sai è che si tratta di un HList
(oltre ad anteporre valori ad esso) e di solito scrivi sempre HList
come un vincolo di tipo.
Nel tuo caso, quello che stai descrivendo è una specie di sequenza allineata al testo. Prima di andare avanti con questo approccio, suggerirei di essere davvero sicuro di aver effettivamente bisogno. Una delle cose belle delle funzioni (e dei tipi di funzioni come il tuo Conversion
) è che esse compongono: tu hai un A => B
e un B => C
e li componi in un A => C
e puoi dimenticarti di B
per sempre. Ottieni una bella scatola nera pulita, che è generalmente esattamente ciò che desideri.
In alcuni casi, tuttavia, può essere utile essere in grado di comporre oggetti simili a funzioni in modo da poter riflettere sui pezzi della pipeline. Presumo che questo sia uno di quei casi, ma dovresti confermarlo per te stesso. Se non lo è, sei fortunato, perché quello che sta arrivando è un po 'disordinato.
darò per scontato questo tipo:
trait Convertable
trait Conversion[A <: Convertable, B <: Convertable] {
def convert(a: A): B
}
possiamo definire una classe tipo che testimoni che una specifica HList
è composto da una o più conversioni i cui tipi allineare:
import shapeless._
trait TypeAligned[L <: HList] extends DepFn1[L] {
type I <: Convertable
type O <: Convertable
type Out = Conversion[I, O]
}
L
contiene tutte le informazioni sul tipo della pipeline e I
e O
sono i tipi dei relativi endpoint.
Poi abbiamo bisogno di esempi per questa classe di tipo (si noti che questo deve essere definito insieme con il tratto sopra per i due da companioned):
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))
}
}
}
E ora è possibile scrivere una versione del AutoConversion
che tiene traccia di tutte le informazioni sul tipo sulla pipeline di:
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)
}
E si può usare in questo modo:
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(a.i.toString)
}
val bc: Conversion[AutoB, AutoC] = new Conversion[AutoB, AutoC] {
def convert(b: AutoB): AutoC = AutoC(b.s.lift(3).getOrElse('-'))
}
val conv = new AutoConversion(ab :: bc :: HNil)
E conv
avrà il tipo statico previsto (e implementare Conversion[AutoA, AutoC]
).