使用宏创建列表时推断HList类型

7

我有一个方法,接受一个 HList 并使用它来构建一个类的实例。 我想提供一些简化的语法,隐藏显式的 cons。所以我想从以下语法转换:

MyThingy.describe( 42 :: true :: "string" :: HNil)

to

MyThingy.describe {
  42
  true
  "string"
}

当定义 MyThingy 如下时:

class MyThingy[L <: HList](elems: L)

我尝试使用这个宏

def describe[L <: HList](elements: Unit): MyThingy[L] = macro MyThingyMacros.describeImpl[L]

并且

def describeImpl[L <: shapeless.HList : c.WeakTypeTag](c: Context)(elems: c.Tree): c.Tree = {
  import c.universe._

  def concatHList: PartialFunction[Tree, Tree] = {
    case Block(l, _) =>
      val els = l.reduceRight((x, y) => q"shapeless.::($x,$y)")
      q"$els :: shapeless.HNil"
  }

  concatHList.lift(elems) match {
    case None => c.abort(c.enclosingPosition, "BOOM!")
    case Some(elemsHList) =>
      val tpe = c.typecheck(elemsHList).tpe
      q"new MyThingy[$tpe]($elemsHList)"
  }

}

但是类型检查器出现了问题:

宏展开期间发生异常: scala.reflect.macros.TypecheckException: 推断的类型参数 [Int,Boolean] 不符合方法 apply 的类型参数边界 [H,T <: shapeless.HList]

显然编译器正在尝试从宏展开前的块中推断 [Int, Boolean]。我也不明白为什么它需要两个参数,而 describeMyThing 只需要一个。

有没有一种方法可以通过宏生成的树来驱动类型推断?

2个回答

7
如果您可以接受逗号分隔的参数列表,那么您可以按照shapeless的HList伴生对象apply方法中使用的风格进行操作。
scala> import shapeless._
import shapeless._

scala> object MyThingy {
     |   def describe[P <: Product, L <: HList](p : P)
     |     (implicit gen: Generic.Aux[P, L]) : L = gen.to(p)
     | }
defined object MyThingy

scala> MyThingy.describe(42, true, "String")
res0: this.Repr = 42 :: true :: String :: HNil

scala> res0.head
res1: Int = 42

一般来说,如果有可行的非宏替代方案,建议避免使用宏。

谢谢,这是一个我没有考虑过的巧妙技巧!逗号完全没问题,我很乐意放弃宏解决方案。 - Gabriele Petronella
@MilesSabin,今天我遇到了这种方法的限制,因为我需要23个元素。可爱的Scala元组... - Gabriele Petronella

4
我有点不同意Miles的观点。个人而言,我无法忍受自动元组,并且如果您想在项目中使用-Xlint,他回答中的解决方案会引起大量警告。我绝对同意,在有可行替代方案的情况下应避免宏,但如果我必须在提供语法糖的情况下选择自动元组和宏之间,我会选择宏。
在您的情况下,这不太难 - 您的逻辑只有一个小错误(实际上有两个)。以下内容将正常工作:
import scala.language.experimental.macros
import scala.reflect.macros.whitebox.Context
import shapeless._

class MyThingy[L <: HList](val elems: L)

def describeImpl[L <: HList: c.WeakTypeTag](c: Context)(elems: c.Tree) = {
  import c.universe._

  def concatHList: PartialFunction[Tree, Tree] = {
    case Block(statements, last) =>
      statements.foldRight(q"$last :: shapeless.HNil")(
        (h, t) => q"shapeless.::($h, $t)"
      )
  }

  concatHList.lift(elems) match {
    case None => c.abort(c.enclosingPosition, "BOOM!")
    case Some(elemsHList) =>
      val tpe = c.typecheck(elemsHList).tpe
      q"new MyThingy[$tpe]($elemsHList)"
  }
}

def describe[L <: HList](elems: Any): MyThingy[L] = macro describeImpl[L]

更简洁地说:
def describeImpl[L <: HList: c.WeakTypeTag](c: Context)(elems: c.Tree) = {
  import c.universe._

  elems match {
    case q"{ ..$elems }" =>
      val hlist = elems.foldRight[c.Tree](q"shapeless.HNil: shapeless.HNil")(
        (h, t) => q"shapeless.::($h, $t)"
      )
      q"new MyThingy($hlist)"
    case _ => c.abort(c.enclosingPosition, "BOOM!")
  }
}

最大的问题在于简化——你需要从HNil开始,而不是构建一个无意义的中间物体然后再添加它。你还需要捕获块的表达式,并将其类型定义为Any而不是Unit,以避免值丢失。
(顺便说一句,我有点惊讶这个白盒宏能够工作,在2.11.2版本中可以。)
我个人更喜欢逗号分隔的语法,这也很容易实现:
def describeImpl[L <: HList: c.WeakTypeTag](c: Context)(elems: c.Tree*) = {
  import c.universe._

  val hlist = elems.foldRight[c.Tree](q"shapeless.HNil: shapeless.HNil")(
    (h, t) => q"shapeless.::($h, $t)"
  )

  q"new MyThingy($hlist)"
}

def describe[L <: HList](elems: Any*): MyThingy[L] = macro describeImpl[L]

在这里的使用方式与产品解决方案相同,但没有自动元组操作。


1
在宏和自动元组之间的选择中,我会说后者是两种恶中较小的一种,差距非常大;-) - Miles Sabin
我真是太傻了,感谢你指出这两个错误!关于自动元组和宏的问题,我没有强烈的意见,所以我会花更多时间来决定使用哪种解决方案。与此同时,我会接受这个答案,因为它更直接地回答了问题。再次感谢你们俩! - Gabriele Petronella
Travis,你能详细说明一下什么让你感到惊讶吗? - Eugene Burmako
1
顺便提一下,为了避免 https://issues.scala-lang.org/browse/SI-6840 的变异,有必要将描述的签名更改为 def describe[L <: HList, T](elems: T*): MyThingy[L] = ... - Eugene Burmako
1
这里有相当多的卫生问题。这是非宏解决方案中无需担心的另一件事情。 - Miles Sabin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接