使用 Shapeless 通用默认实例构建 case 类,并使用定义通用 createValue 方法的参数

Build, using Shapeless, generic default instances for case classes with parameters defining a common createValue method

我正在努力实现以下目标 - 使用 shapeless 似乎是一个不错的途径。

给定当前 class 模型:

import shapeless._

object ShapelessTest {
  case class Definition[T](id: String) extends Typeable[T] {
    type V = Value[T]

    override def cast(t: Any): Option[T] = createValue(t.asInstanceOf[Option[T]]).value

    override def describe: String = s"$id"

    def createValue(value: Option[T]): V =
      Value[T](this, value)
  }

  case class Value[T](definition: Definition[T], value: Option[T])

  val DefA: Definition[Int] = Definition[Int]("defA")
  val DefB: Definition[String] = Definition[String]("defB")

  case class Instance(valA: DefA.V,
                      valB: DefB.V)

  def main(args: Array[String]): Unit = {
     val Empty: Instance = Instance(
       DefA.createValue(None),
       DefB.createValue(None)
     )
    println(s"Empty manual: $Empty")

    val emptyHl = Generic[Instance].from(DefA.createValue(None) :: DefB.createValue(None) :: HNil)
    println(s"Empty hlist: $emptyHl")
  }
}

我可以创建一个空实例作为 Empty 实例,方法是在正确的定义上手动调用 createValue 方法,或者使用 shapeless 转换 HList。

我想弄清楚是否有可能以编程方式为每个 class 具有类型 Value.

的字段创建一个 Empty 实例

换句话说,我希望能够调用

val Empty: Instance = empty[Instance]

并且具有与 emptyHlEmpty 实例相同的结果。

这似乎类似于无形指南中的“8.3 随机值生成器”示例,但不是生成随机数,而是使用案例 class 中存在的每种类型的函数,我正在尝试具体化每个参数的具体 Definition 类型,并对其调用 createValue(None) 方法。

我试了很多都没有成功。

使用 hlist.Mapper 和在 Typeable 上定义的 Poly1,我可以获得参数列表,但我无法调用任何方法在 typeable 上。

任何帮助将不胜感激,谢谢! :)


Update 9 Apr

我能够想出一个非常复杂的解决方案 - 不幸的是有很多铸造但完成了工作。

我想对此进行迭代并使其变得更好。我尝试使用 natMapper: NatTRel 但我无法让它在单例类型上工作。 我相信这可以做得更好!欢迎任何建议。

import shapeless.ops.hlist
import shapeless.ops.hlist.{Comapped, Reify}
import shapeless.{Generic, HList, HNil}

object ShapelessTest2 {

  case class Definition[T](id: String) {
    type V = Value[this.type]

    def createValue(value: Option[T]) =
      new Value[this.type] {
        type NT = T
        override val valueT: Option[T] = value
        override val attrDef: Definition.this.type = Definition.this
      }
  }

  trait Value[D] {
    type NT
    val attrDef: D
    val valueT: Option[NT]
  }

  object DefA extends Definition[Int]("defA")
  object DefB extends Definition[Int]("defB")
  object DefC extends Definition[String]("defC")

  case class Instance(valA: DefA.V,
                      valB: DefB.V,
                      valC: DefC.V)

  // Compile safe
  val Inst1: Instance = Instance(
    DefA.createValue(Some(1)),
    DefB.createValue(Some(2)),
    DefC.createValue(Some("2"))
  )

  def main(args: Array[String]): Unit = {
    def empty[A <: Product] = new PartiallyApplied[A]

    class PartiallyApplied[A <: Product] {
      def apply[
          V <: HList,
          DL <: HList,
          RDL <: HList,
          H <: Definition[_],
          T <: HList
      ]()(
          implicit
          gen: Generic.Aux[A, V],
          comapped: Comapped.Aux[V, Value, DL],
          reify: Reify.Aux[DL, RDL],
          isHCons: hlist.IsHCons.Aux[RDL, H, T],
      ): A = {
        def getEmpties[L](list: RDL): V = {
          val hlist = list match {
            case HNil => HNil
            case _ => list.head.createValue(None) :: getEmpties(list.tail.asInstanceOf[RDL])
          }
          hlist.asInstanceOf[V]
        }

        val empties = getEmpties(reify.apply())
        gen.from(empties)
      }
    }

    val emptyInstance = empty[Instance]()
    println(s"Empty valA: ${emptyInstance.valA.attrDef} - ${emptyInstance.valA.valueT}")
    println(s"Empty valB: ${emptyInstance.valB.attrDef} - ${emptyInstance.valB.valueT}")
    println(s"Empty valC: ${emptyInstance.valC.attrDef} - ${emptyInstance.valC.valueT}")
  }
}

正确打印

Empty valA: Definition(defA) - None
Empty valB: Definition(defB) - None
Empty valC: Definition(defC) - None

根本不用 Typeable 就可以更轻松地实现你想做的事情(顺便说一句,Typeable[T] 应该用作类型 class 而不是混合)。在 ggeneral 中,派生类型 classes with shapeless 的模式如下所示:

import shapeless._

trait Empty[A] {
  def createValue(): A
}
object Empty {
  // summoning method
  def apply[T](implicit empty: Empty[T]): T = empty.createValue()

  // implicits for HNil and HCons build HList if we provide the right implicits

  implicit val emptyHNil: Empty[HNil] = () => HNil

  implicit def emptyHCons[H, T](
    implicit 
    head: Empty[H],
    tail: Empty[T]
  ): Empty[H :: T] = () => head.createValue() :: tail.createValue()

  // Generic will translate it HList into Product type

  implicit def emptyProduct[T, Rep](
    implicit
    gen: Generic.Aux[T, Rep],
    empty: Empty[Rep]
  ): Empty[T] = () => gen.from(empty.createValue())

  // if you need default instances accessible everywhere without import put them here
}

用作

case class Instance(int: Int, string: String)
implicit int: Empty[Int] = () => 0
implicit int: Empty[String] = () => ""
Empty[Instance].createValue[Instance] // Instance(0, "")

在您的情况下,您必须使 Value[T]s 在隐式范围内可用,并从较小的值派生 Value[Instance](如上面的 Empty 示例)。如何在隐式作用域中提供基本 Value 实例取决于您:您可以将它们放入伴随对象中,或者手动定义或从隐式作用域中已经存在的其他类型派生。

长话短说,你想要的应该是可能的,但你必须先将事情放入隐式范围。

我猜你不知何故滥用了 Typeable。使用 Typeable 的想法是进行类型安全转换。但是你回到 asInstanceOf.

Typeable 是一个类型 class。所以你应该使用你的 Definition 作为类型 class。使 DefADefB、...隐含。

implicit val DefA: Definition[Int] = Definition[Int]("defA")
implicit val DefB: Definition[String] = Definition[String]("defB")

def empty[A <: Product] = new PartiallyApplied[A]

class PartiallyApplied[A <: Product] {
  def apply[Vs <: HList, L <: HList, Ds <: HList]()(implicit
    gen: Generic.Aux[A, Vs],
    comapped: Comapped.Aux[Vs, Value, L],
    liftAll: LiftAll.Aux[Definition, L, Ds],
    natMapper: NatTRel[Ds, Definition, Vs, Value],
  ): A = {
    object createValueNatTransform extends (Definition ~> Value) {
      override def apply[T](definition: Definition[T]): Value[T] =
        definition.createValue(None)
    }

    gen.from(natMapper.map(createValueNatTransform, liftAll.instances))
  }
}

val Empty: Instance = empty[Instance]() 
// Instance(Value(Typeable[defA],None),Value(Typeable[defB],None))

使用您的原始代码的宏

def empty[A]: A = macro emptyImpl[A]

def emptyImpl[A: c.WeakTypeTag](c: blackbox.Context): c.Tree = {
  import c.universe._
  val typA = weakTypeOf[A]
  val trees = typA.decls.filter(_.asTerm.isVal).map(_.infoIn(typA) match {
    case TypeRef(pre, _, _) => q"${pre.termSymbol}.createValue(_root_.scala.None)"
  })
  q"new $typA(..$trees)"
}

val Empty: Instance = empty[Instance]

//Warning:scalac: performing macro expansion App.empty[App.Instance]
//Warning:scalac: new App.Instance(DefA.createValue(_root_.scala.None),
//                                 DefB.createValue(_root_.scala.None))

// Instance(Value(Typeable[defA],None),Value(Typeable[defB],None))

关于您的新代码使 Value 协变并使用 Mapper(对于正确定义的 Poly)而不是 NatTRel 或运行时递归

trait Value[+D] {
  type NT
  val attrDef: D
  val valueT: Option[NT]
}

object createValuePoly extends Poly1 {
  implicit def cse[D <: Definition[T] with Singleton, T](implicit
    ev: D <:< Definition[T]): Case.Aux[D, Value[D]] = at(_.createValue(None))
}

def empty[A <: Product] = new PartiallyApplied[A]

class PartiallyApplied[A <: Product] {
  def apply[
      V <: HList,
      DL <: HList,
  ]()(
      implicit
      gen: Generic.Aux[A, V],
      comapped: Comapped.Aux[V, Value, DL],
      reify: Reify.Aux[DL, DL],
      mapper: Mapper.Aux[createValuePoly.type, DL, V]
  ): A = gen.from(mapper(reify()))
}

val emptyInstance = empty[Instance]()
println(s"Empty valA: ${emptyInstance.valA.attrDef} - ${emptyInstance.valA.valueT}") 
//Empty valA: Definition(defA) - None
println(s"Empty valB: ${emptyInstance.valB.attrDef} - ${emptyInstance.valB.valueT}") 
//Empty valB: Definition(defB) - None
println(s"Empty valC: ${emptyInstance.valC.attrDef} - ${emptyInstance.valC.valueT}") 
//Empty valC: Definition(defC) - None