Masz rację, żądając więcej informacji o typie, a ogólnie, jeśli masz wartość z HList
jako typu statycznego, prawdopodobnie będziesz musiał zmienić swoje podejście. Zasadniczo nie można nic zrobić z HList
, jeśli wszystko, co wiesz, to to, że jest to HList
(oprócz tego, że przedkłada do niego wartości), i zazwyczaj piszesz tylko HList
jako ograniczenie typu.
W twoim przypadku to, co opisujesz, jest rodzajem sekwencji wyrównanej do typu. Zanim zastosujesz to podejście, sugeruję, że naprawdę musisz to zrobić. Jedną z fajnych rzeczy na temat funkcji (i typów funkcjonalnych, takich jak Twoja Conversion
) jest to, że komponują się: masz A => B
i B => C
, a Ty komponujesz je jako A => C
i możesz na zawsze zapomnieć o B
. Dostajesz ładną czystą czarną skrzynkę, która jest dokładnie tym, czego potrzebujesz.
W niektórych przypadkach przydatne może być komponowanie rzeczy funkcjonalnych w taki sposób, aby można było zastanowić się nad kawałkami potoku. Zakładam, że jest to jeden z tych przypadków, ale powinieneś to potwierdzić dla siebie. Jeśli nie, masz szczęście, bo to, co nadchodzi, jest trochę nieporządne.
będę zakładać te typy:
trait Convertable
trait Conversion[A <: Convertable, B <: Convertable] {
def convert(a: A): B
}
Możemy zdefiniować klasę typu, że świadkowie, że konkretny HList
składa się z jednego lub większej liczby konwersji, których typy linii:
import shapeless._
trait TypeAligned[L <: HList] extends DepFn1[L] {
type I <: Convertable
type O <: Convertable
type Out = Conversion[I, O]
}
L
zawiera wszystkie informacje o typie potoku, a I
i O
są typami jego punktów końcowych.
Następnie musimy instancje tej klasy typu (zauważ, że to musi być zdefiniowane wraz z cechą wyżej dla dwóch do 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))
}
}
}
I teraz można napisać wersję swojej AutoConversion
że śledzi wszystkie informacje na temat typu rurociągu:
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)
}
I można go używać tak:
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)
i conv
będzie mieć oczekiwany typ statyczny (i implementować Conversion[AutoA, AutoC]
).