Sie haben recht, wenn Sie mehr Typinformationen benötigen, und im Allgemeinen, wenn Sie einen Wert mit HList
als haben ein statischer Typ, ist es wahrscheinlich, dass Sie Ihren Ansatz ändern müssen. Es gibt im Wesentlichen nichts, was Sie mit einem HList
tun können, wenn alles, was Sie wissen, ist, dass es ein HList
ist (neben den Werten vor), und Sie werden immer nur HList
als Typ Einschränkung schreiben.
In Ihrem Fall ist das, was Sie beschreiben, eine Art typisierte Sequenz. Bevor Sie mit diesem Ansatz fortfahren, würde ich vorschlagen, dass Sie wirklich sicher sind, dass Sie das wirklich tun müssen. Eines der schönen Dinge über Funktionen (und Funktion ähnliche Typen wie Ihre Conversion
) ist, dass sie komponieren: Sie haben eine A => B
und eine B => C
und Sie komponieren sie in eine A => C
und können über B
für immer vergessen. Sie erhalten eine schöne, saubere schwarze Box, die in der Regel genau das ist, was Sie wollen.
In einigen Fällen kann es jedoch nützlich sein, funktionsähnliche Dinge so zu komponieren, dass Sie über die Teile der Pipeline nachdenken können. Ich gehe davon aus, dass dies einer dieser Fälle ist, aber Sie sollten das für sich selbst bestätigen. Wenn nicht, hast du Glück, denn was kommt, ist irgendwie unordentlich.
Ich werde diese Typen annehmen:
trait Convertable
trait Conversion[A <: Convertable, B <: Convertable] {
def convert(a: A): B
}
Wir können eine Typklasse definieren, die Zeugen, dass eine bestimmte HList
von einem oder mehreren Umwandlungen, deren Typen in einer Reihe aufstellen zusammen:
import shapeless._
trait TypeAligned[L <: HList] extends DepFn1[L] {
type I <: Convertable
type O <: Convertable
type Out = Conversion[I, O]
}
L
enthält alle Typinformationen über die Pipeline, und I
und O
sind die Typen seiner Endpunkte.
Als nächstes müssen wir Instanzen für diese Klasse (beachten Sie, dass dies muss zusammen mit dem obigen Merkmale definiert werden, für die zwei companioned werden):
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))
}
}
}
Und jetzt können Sie eine Version Ihrer AutoConversion
schreiben, dass verfolgt alle von der Art, Informationen über die 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)
}
Und Sie es wie folgt verwenden können:
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)
Und conv
haben den erwarteten statischen Typ (und implementieren Conversion[AutoA, AutoC]
).