【问题标题】:How to use Scala macros to create new partial functions or transform them?如何使用 Scala 宏来创建新的部分函数或转换它们?
【发布时间】:2017-01-08 23:24:28
【问题描述】:

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

产生相同错误的最简单问题是将给定的部分函数分解为 binder、guard 和 body,然后将其重新组合成相同的部分函数的代码。

我可以使用简单类型 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

非工作代码:完全相同的宏,除了使用案例类而不是 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 )

我已将问题简化为仍然失败的最低限度。在我的实际代码中,类型更复杂,部分函数可能有保护,我确实通过重新排列它的参数和保护来转换部分函数的代码。我有时可以在没有守卫的情况下进行转换,但当偏函数的参数是案例类时则不行。也许守卫的存在并不是问题的根源:当某处存在带有unapply 的复合类型时,就会出现问题。我收到的错误消息与上面显示的这个大大简化的示例基本相同。

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

  • 使用白盒宏上下文
  • 使用普通的准引号,如上例所示
  • 对案例和模式 cq"..."pq"..."q"{case ..$cases}" 使用特殊的准引号,如准引号文档中所示
  • 与后卫匹配:q"{case $binder if $guard =&gt; $body }",也与cqpq quasiquotes 匹配
  • 在各个地方添加c.typecheckc.untypecheck(以前称为resetAllAttrs,现在已弃用)
  • 不使用准引号,而是在原始语法树中执行所有操作:使用 Traverser 与原始树匹配,例如 case UnApply(Apply(Select(t@Ident(TermName(_)), TermName("unapply")), List(Ident(TermName("&lt;unapply-selector&gt;")))), List(binder)) if t.tpe &lt;:&lt; typeOf[Blob]
  • 尝试将模式匹配器中的 Ident 替换为来自保护条件的 Ident,反之亦然(这会产生奇怪的错误,“断言失败”,由于类型检查失败)
  • 使用Any代替具体类型,返回PartialFunction[Any,Any],或者总函数Function1[Blob,Any]
  • 使用类型参数T而不是Blob,参数化宏并接受PartialFunction[T,Any]

如果有任何帮助,我将不胜感激!我正在使用带有直接宏的 Scala 2.11.8(没有“宏天堂”)。

【问题讨论】:

  • { case x@Blob(aaa: Int, bbb: Int) =&gt; x.left + x.right } 这是工作,我也不知道为什么-_-

标签: scala macros scala-macros scala-quasiquotes partial-functions


【解决方案1】:

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

def 宏和类型检查的问题

Def 宏在类型检查阶段进行扩展。因此,def 宏的参数是类型树。返回一个类型良好的无类型的树是可以接受的。但是,返回 部分类型(您的情况)或 错误类型 树很可能会使编译器跳闸,最多导致类型检查错误或后续阶段的错误.请注意,准引号会生成 untyped 树。这些坏树怎么会出现?

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

解决方法

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

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

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

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

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

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

【讨论】:

  • 所以简单的解决方案将是以下两个选项之一:1)在操作它们之前取消类型检查所有宏参数(有丢失一些所需类型信息的风险?),或者在之前只取消最后一棵树的类型检查宏返回它,2)在宏内使用的每个准引号之后对所有内容进行类型检查?哪一个效果更好?
  • 到目前为止有效的方法:1)在宏体的末尾写c.untypecheck(q"...")。这将消除错误,并且宏可以正常工作。 2) 将类型注释插入活页夹,然后再次取消类型检查。本练习的要点:最好避免拆卸和重新组装表达式树。宏应该创建一个全新的表达式树,或者对现有的有效表达式树进行有限的搜索和替换,重新排列其现有元素。向现有表达式树添加新片段很容易出错。
猜你喜欢
  • 1970-01-01
  • 2013-08-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-05-14
  • 2013-06-28
  • 1970-01-01
  • 2019-07-22
相关资源
最近更新 更多