如何使用Scala宏创建新的部分函数或对其进行转换



我在编写转换给定偏函数并创建新的偏函数的宏时遇到问题。例如,我希望能够将给定的偏函数分解为其元素——模式绑定器、保护条件和主体;然后我想把模式绑定器和保护条件分解成更小的部分,并从这些部分重新组装新的部分函数。然而,我在宏扩展时遇到了一些奇怪的错误,无法调试。

给出相同错误的最简单问题是代码将给定的部分函数分解为绑定器、保护器和主体,并将其重新组合为相同的部分函数。

我可以用简单的类型PartialFunction[Int,Any]来实现这一点,但不能用涉及事例类PartialFunction[MyCaseClass,Any]的类型来实现。

以下是有效的代码和无效的代码。

工作代码:取一个分部函数,使用准引号对其进行解构,再次组装相同的函数,并返回它。

package sample
import scala.language.experimental.macros
import scala.reflect.macros.blackbox
object MacroTest {
type Simple = PartialFunction[Int, Any]
def no_problem(pf: Simple): Simple = macro no_problemImpl
def no_problemImpl(c: blackbox.Context)(pf: c.Expr[Simple]) = {
import c.universe._
val q"{ case $binder  => $body }" = pf.tree
q"{ case $binder  => $body }"
}
}

此宏编译并测试通过:

import MacroTest._
val testPf: Simple = { case x => x + 1 }
testPf(2) shouldEqual 3 // passes
// now do the same with macro:
val result = no_problem({ case x => x + 1 })
result(2) shouldEqual 3 // passes

非工作代码:完全相同的宏,只是使用case类而不是Int作为部分函数的参数。

case class Blob(left: Int, right: Int)
type Complicated = PartialFunction[Blob, Any]
def problem(pf: Complicated): Complicated = macro problemImpl
def problemImpl(c: blackbox.Context)(pf: c.Expr[Complicated]) = {
import c.universe._
val q"{ case $binder  => $body }" = pf.tree
q"{ case $binder  => $body }"
}

代码是完全相同的,只是类型不同(Complicated而不是Simple)。

宏代码编译,但测试未能编译(在宏扩展时失败):

val blob = Blob(1,2)
val testPf: Complicated = { case Blob(x, y) => x + y }
testPf(blob) shouldEqual 3 // passes
// now try the same with macro:
val result = problem({ case Blob(x, y) => x + y })
// compile error when compiling the test code: 
Could not find proxy for case val x1: sample.Blob in List(value x1, method applyOrElse, <$anon: Function1>, value result, method apply, <$anon: Function0>, value <local MacroTestSpec>, class MacroTestSpec, package sample, package <root>) (currentOwner= value y )

我已经把这个问题简化到最低限度,但仍然失败。在我的实际代码中,类型更复杂,分部函数可能有保护,我通过重新排列其参数和保护来转换分部函数的代码。有时,当没有保护时,我可以使转换工作,但当分部函数的参数是case类时,就不能了。也许警卫的存在并不是问题的根源:当某个地方有一个带有unapply的复合类型时,就会出现问题。我得到的错误信息基本上与上面显示的这个非常简化的示例相同。

尽管我尝试了许多转换部分函数的替代方法,但似乎无法解决这个问题:

  • 使用白盒宏上下文
  • 使用纯准引号,如上面显示的示例所示
  • 对事例和模式cq"..."pq"..."q"{case ..$cases}"使用特殊的准引号,如准引号文档所示
  • 与保护匹配:q"{case $binder if $guard => $body }",也与cqpq准引号匹配
  • 在不同位置添加c.typecheckc.untypecheck(以前称为resetAllAttrs,现在已弃用)
  • 不使用准引号,但在原始语法树中执行所有操作:使用具有原始树匹配的Traverser,如case UnApply(Apply(Select(t@Ident(TermName(_)), TermName("unapply")), List(Ident(TermName("<unapply-selector>")))), List(binder)) if t.tpe <:< typeOf[Blob]
  • 尝试用从保护条件中获取的Ident替换模式匹配器中的Ident,反之亦然(由于类型检查失败,这会产生奇怪的错误,即"断言失败")
  • 使用Any而不是特定类型,返回PartialFunction[Any,Any]或总函数Function1[Blob,Any],依此类推
  • 使用类型参数T而不是Blob,参数化宏并接受PartialFunction[T,Any]

如有任何帮助,我将不胜感激!我使用的是带有直接宏的Scala2.11.8(没有"宏天堂")。

我相信您在Scala编译器中遇到了一个长期存在的问题。在一些情况下,类型检查不是幂等,特别是使用unapply:SI-5465的提取器。对此没有简单的解决方案,但我可以建议两种解决方案。让我先简单解释一下这个问题。

def宏和类型检查的问题

Def宏在类型检查阶段展开。因此,定义宏的参数是类型树。返回良好类型的非类型的树是可以接受的。但是,返回部分类型的(您的情况)或类型错误的类型化树。这些坏树是怎么发生的?

  • 部分类型的树-通常通过将非类型的代码包装在类型化参数周围,或用未类型化代码替换部分参数。在许多情况下,你可以逃脱惩罚,但并非总是如此
  • 类型错误的树-通过以使原始类型信息无效的方式重新排列类型的参数,例如将一个参数拼接到另一个参数。这些肯定会造成问题

解决方法

希望你能看到这个问题是概念性的,而且根深蒂固。但你可以采取两种方法之一来解决这个问题:

  1. 破解解决方案-通过字符串表示的最终结果进行往返:

    c.parse(showCode(q"{ case $binder  => $body }"))
    

    showCode通常会打印可解析的代码,即使untypecheck不是幂等的。当然,这将导致一些编译时性能开销,这对于您的用例来说可能是可接受的,也可能是不可接受的。

  2. 硬解决方案-使用内部编译器API手动对粘合代码进行类型检查。我无法在一篇文章中解释如何做到这一点,但你必须了解所有类型、符号及其所有者。最糟糕的部分是树是可变的wrt类型信息。如果你走这条路,我建议你浏览scala/async的源代码

最好的做法可能是避免编写宏,或者等到scala.meta的语义API发布后,您可以将其用于def宏。

相关内容

  • 没有找到相关文章

最新更新