В общем случае преобразовать класс в другой, используя Shapeless + LabelledGenerics
Я знаю, используя Shapeless, я могу сделать что-то вроде этого:
import shapeless._, syntax.singleton._, record._
case class Foo(x: Int, y: String)
case class RichFoo(x: Int, y: String, z: Double)
def makeRich(foo: Foo): RichFoo = {
val x = ('z ->> 0.9)
val repr = LabelledGeneric[Foo].to(foo) + x
LabelledGeneric[RichFoo].from(repr)
}
val a = Foo(1, "hello")
val b = makeRich(a)
Теперь я хочу написать общий способ сделать это:
trait Morph[A, B, AR, BR] {
def apply(a: A)(f: AR => BR): B
}
object Morph {
implicit def genericMorph[A, B, AR, BR](implicit genA: LabelledGeneric.Aux[A, AR], genB: LabelledGeneric.Aux[B, BR]): Morph[A, B, AR, BR] =
new Morph[A, B, AR, BR] {
override def apply(a: A)(f: AR => BR) = genB.from(f(genA.to(a)))
}
implicit class Syntax[A](a: A) {
def morph[AR, BR, B](f: AR => BR)(implicit morph: Morph[A, B, AR, BR]): B =
morph(a)(f)
}
}
Но теперь использование unweidly?
val a = Foo(1, "hello")
a.morph[???, ???, RichFoo](_ + ('z ->> 0.9))
Как лучше разработать этот API?
Я попробовал что-то вроде этого:
implicit class Syntax[A](a: A) {
def morphTo[B] = new {
def using[AR <: HList, BR <: HList](f: AR => BR)(implicit morph: Morph[A, B, AR, BR]): B =
morph(a)(f)
}
}
a.morphTo[RichFoo].using(_ :+ ('z ->> 0.9))
Но это на самом деле не работает
3 ответа
Есть два ограничения, которые мешают выводу типов работать так, как вы хотите в вашем примере (оба не имеют ничего общего с бесформенным btw):
В течении
scalac
явное указание параметров типа - это все или ничего. Но вы хотите указать толькоB
оставляя остальное для вывода. Карри является одним из решений этой проблемы. Итак, ваша попытка была на правильном пути, но не учитывала 2.Вывод типа для параметров метода течет слева направо по одному списку параметров за раз. Но вы хотите сделать вывод о типе
f
в зависимости от типаmorph
который идет последним, потому что он неявный. Решение здесь... Снова карри.
Итак, из 1. и 2. следует, что вы должны карри дважды:
implicit class Syntax[A](a: A) {
def morphTo[B] = new {
def by[AR <: HList, BR <: HList](implicit morph: Morph[A, B, AR, BR]) = new {
def using(f: AR => BR): B = morph(a)(f)
}
}
}
a.morphTo[RichFoo].by.using(_ :+ ('z ->> 0.9))
Существует альтернативное решение 1. - используйте фиктивный аргумент для указания параметра типа B
:
trait To[-A]
object To {
private val instance = new To[Any] { }
def apply[A]: To[A] = instance
}
implicit class Syntax[A](a: A) {
def morph[B, AR <: HList, BR <: HList](to: To[B])(
implicit morph: Morph[A, B, AR, BR]
) = new {
def using(f: AR => BR): B = morph(a)(f)
}
}
a morph To[RichFoo] using (_ :+ ('z ->> 0.9))
Для дальнейшего использования этих проблем в Dotty:
- Вы уже можете частично указать параметры типа:
a.morph[B = RichFoo]
- Продолжается работа над более согласованным неявным синтаксисом параметров: lampepfl / dotty # 1260
Изменить: Обычно хорошей идеей является определение типов, которые зависят от других типов, как членов типа:
trait Morph[A, B] {
type AR
type BR
def apply(a: A)(f: AR => BR): B
}
object Morph {
type Aux[A, B, AR0, BR0] = Morph[A, B] {
type AR = AR0
type BR = BR0
}
implicit def genericMorph[A, B, AR0, BR0](
implicit genA: LabelledGeneric.Aux[A, AR0], genB: LabelledGeneric.Aux[B, BR0]
): Aux[A, B, AR0, BR0] = new Morph[A, B] {
type AR = AR0
type BR = BR0
def apply(a: A)(f: AR => BR) = genB.from(f(genA.to(a)))
}
implicit class Syntax[A](a: A) {
def morphTo[B](implicit morph: Morph[A, B]) = new {
def using(f: morph.AR => morph.BR) = morph(a)(f)
}
}
}
import shapeless._, syntax.singleton._, record._
case class Foo(x: Int, y: String)
case class RichFoo(x: Int, y: String, z: Double)
class Morph[A, B, AR](a: A, genA: LabelledGeneric.Aux[A, AR]) {
def apply[BR](f: AR => BR)(implicit genB: LabelledGeneric.Aux[B, BR]) = genB.from(f(genA.to(a)))
}
implicit class Syntax[A, AR](val a: A)(implicit genA: LabelledGeneric.Aux[A, AR]) {
def morph[B]: Morph[A, B, AR] = new Morph(a, genA)
}
val a = Foo(1, "hello")
a.morph[RichFoo](_ + ('z ->> 0.9)) // => RichFoo(1,hello,0.9)
Основываясь на ответе @g.krastev, я выбрал такой подход DSL:
import shapeless._, syntax.singleton._, record._, ops.hlist._
case class Morph[A, AR](a: A)(implicit reprA: LabelledGeneric.Aux[A, AR]) {
def to[B] = new {
def apply[BR](f: AR => BR)(implicit reprB: LabelledGeneric.Aux[B, BR]): B =
reprB.from(f(reprA.to(a)))
}
}
Тогда мы можем использовать это как:
val a = Foo(1, "hello")
val b = Morph(a).to[RichFoo](_ + ('z ->> 0.9)) // => RichFoo(1,hello,0.9)
Мы также можем сделать так, чтобы он переупорядочивал поля следующим образом:
case class Morph[A, AR](a: A)(implicit reprA: LabelledGeneric.Aux[A, AR]) {
def to[B] = new {
def apply[BR <: HList, BR2 <: HList](f: AR => BR2)(implicit reprB: LabelledGeneric.Aux[B, BR], align: Align[BR2, BR]): B =
reprB.from(align(f(reprA.to(a))))
}
}