使用 Shapeless + LabelledGenerics 一般地将一个类变形为另一个类

pat*_*rit 3 scala type-level-computation shapeless labelled-generic

我知道使用 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)
Run Code Online (Sandbox Code Playgroud)

现在我想写一个通用的方法来做到这一点:

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)
  }
}
Run Code Online (Sandbox Code Playgroud)

但是,现在的用法是不是很奇怪?

  val a = Foo(1, "hello")
  a.morph[???, ???, RichFoo](_ + ('z ->> 0.9))
Run Code Online (Sandbox Code Playgroud)

设计此 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))
Run Code Online (Sandbox Code Playgroud)

但它并没有真正起作用

g.k*_*tev 5

有两个限制会阻止类型推断在您的示例中以您想要的方式工作(两者都与无形的 btw 无关):

  1. 在当前scalac明确指定类型参数是全有或全无。但是您只想指定B其余部分进行推断。柯里化是解决这个问题的一种方法。所以你的尝试是在正确的轨道上,但没有考虑到 2。

  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))
Run Code Online (Sandbox Code Playgroud)

有一个替代解决方案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))
Run Code Online (Sandbox Code Playgroud)

以供将来参考Dotty 中如何解决这些问题:

  1. 您已经可以部分指定类型参数: a.morph[B = RichFoo]
  2. 正在进行更一致的隐式参数语法的工作: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)
    }
  }
}
Run Code Online (Sandbox Code Playgroud)