如何使用 Scala 宏创建新的部分函数或转换它们?
How to use Scala macros to create new partial functions or transform them?
我在编写转换给定偏函数并创建新偏函数的宏时遇到问题。例如,我希望能够将给定的部分函数分解为其元素——模式绑定器、保护条件和主体;然后我想将模式绑定器和保护条件分解成更小的部分,并从这些部分中重新 assemble 新的部分函数。但是,我在宏扩展时遇到无法调试的奇怪错误。
给出相同错误的最简单问题是将给定的部分函数分解为绑定器、守卫和主体,然后将其重新assemble返回到相同的部分函数的代码。
我可以使用简单类型 PartialFunction[Int,Any]
但不能使用涉及大小写 classes、PartialFunction[MyCaseClass,Any]
.
的类型
这是有效的代码和无效的代码。
工作代码:采用部分函数,使用准引号对其进行解构,assemble再次使用相同的函数,然后return它。
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
非工作代码:完全相同的宏,除了使用大小写 class 而不是 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 class 时就不行了。也许守卫的存在不是问题的根源:当某处存在带有 unapply
的复合类型时,问题就会发生。我得到的错误消息与我在上面显示的这个大大简化的示例中得到的错误消息基本相同。
尽管尝试了很多转换部分函数的替代方法,但我似乎无法解决这个问题:
- 使用白盒宏上下文
- 使用普通的准引号,如上例所示
- 如准引号文档中所示
cq"..."
、pq"..."
和 q"{case ..$cases}"
对案例和模式使用特殊的准引号
- 匹配守卫:
q"{case $binder if $guard => $body }"
,也匹配cq
和pq
准引号
- 在各个地方添加
c.typecheck
或c.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
,反之亦然(这会产生奇怪的错误,"assertion failed",由于类型检查失败)
- 使用
Any
代替具体的类型,returning PartialFunction[Any,Any]
,或者总函数Function1[Blob,Any]
等等
- 使用类型参数
T
而不是 Blob
,参数化宏并接受 PartialFunction[T,Any]
如有任何帮助,我将不胜感激!我正在使用带有直接宏的 Scala 2.11.8(没有 "macro paradise")。
我相信您遇到了 Scala 编译器中一个长期存在的问题。在某些情况下,类型检查不是 idempotent,特别是使用 unapply
的提取器:SI-5465。对此没有简单的解决方案,但我可以建议两种解决方法。先把问题简单说明一下。
def 宏和类型检查的问题
Def 宏在类型检查阶段被扩展。因此,def 宏的参数是类型树。返回 well-typed 或 untyped 树是可以接受的。但是,返回 partially typed (你的情况)或 ill-typed 树很可能会使编译器出错,最多导致类型检查错误或后续阶段的错误。请注意,quasiquotes 生成 untyped 树。怎么会出现这些烂树?
- 部分类型树 - 通常通过将 非类型 代码包装在 类型 参数周围或替换其中一部分带有 未键入的 代码。在许多情况下,您可以摆脱这些,但并非总是如此。
- 类型错误的树 - 通过重新排列 typed 参数使原始类型信息无效,例如将一个论点拼接成另一个论点。这些肯定会出问题。
解决方法
希望你能看出问题是概念性的,是根深蒂固的。但是您可以采用以下两种方法之一来解决问题:
hacky 解决方案 - 通过最终结果的字符串表示进行往返:
c.parse(showCode(q"{ case $binder => $body }"))
showCode
通常会打印可解析的代码,即使 untypecheck
不是幂等的。当然,这会产生一些编译时性能开销,这对于您的用例来说可能是可以接受的,也可能是不可以接受的。
- 硬解 - 使用内部编译器APIs 手动检查粘合代码。我无法在一个 post 中解释如何做到这一点,但您必须学习所有关于类型、符号及其所有者的知识。最糟糕的是树是 可变的 wrt 类型信息。如果您走那条路,我建议您阅读 scala/async.
的源代码
最好的办法可能是避免编写宏,或者等到 scala.meta
的语义 API 发布,你可以将它用于 def 宏。
我在编写转换给定偏函数并创建新偏函数的宏时遇到问题。例如,我希望能够将给定的部分函数分解为其元素——模式绑定器、保护条件和主体;然后我想将模式绑定器和保护条件分解成更小的部分,并从这些部分中重新 assemble 新的部分函数。但是,我在宏扩展时遇到无法调试的奇怪错误。
给出相同错误的最简单问题是将给定的部分函数分解为绑定器、守卫和主体,然后将其重新assemble返回到相同的部分函数的代码。
我可以使用简单类型 PartialFunction[Int,Any]
但不能使用涉及大小写 classes、PartialFunction[MyCaseClass,Any]
.
这是有效的代码和无效的代码。
工作代码:采用部分函数,使用准引号对其进行解构,assemble再次使用相同的函数,然后return它。
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
非工作代码:完全相同的宏,除了使用大小写 class 而不是 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 class 时就不行了。也许守卫的存在不是问题的根源:当某处存在带有 unapply
的复合类型时,问题就会发生。我得到的错误消息与我在上面显示的这个大大简化的示例中得到的错误消息基本相同。
尽管尝试了很多转换部分函数的替代方法,但我似乎无法解决这个问题:
- 使用白盒宏上下文
- 使用普通的准引号,如上例所示
- 如准引号文档中所示
cq"..."
、pq"..."
和q"{case ..$cases}"
对案例和模式使用特殊的准引号 - 匹配守卫:
q"{case $binder if $guard => $body }"
,也匹配cq
和pq
准引号 - 在各个地方添加
c.typecheck
或c.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
,反之亦然(这会产生奇怪的错误,"assertion failed",由于类型检查失败) - 使用
Any
代替具体的类型,returningPartialFunction[Any,Any]
,或者总函数Function1[Blob,Any]
等等 - 使用类型参数
T
而不是Blob
,参数化宏并接受PartialFunction[T,Any]
如有任何帮助,我将不胜感激!我正在使用带有直接宏的 Scala 2.11.8(没有 "macro paradise")。
我相信您遇到了 Scala 编译器中一个长期存在的问题。在某些情况下,类型检查不是 idempotent,特别是使用 unapply
的提取器:SI-5465。对此没有简单的解决方案,但我可以建议两种解决方法。先把问题简单说明一下。
def 宏和类型检查的问题
Def 宏在类型检查阶段被扩展。因此,def 宏的参数是类型树。返回 well-typed 或 untyped 树是可以接受的。但是,返回 partially typed (你的情况)或 ill-typed 树很可能会使编译器出错,最多导致类型检查错误或后续阶段的错误。请注意,quasiquotes 生成 untyped 树。怎么会出现这些烂树?
- 部分类型树 - 通常通过将 非类型 代码包装在 类型 参数周围或替换其中一部分带有 未键入的 代码。在许多情况下,您可以摆脱这些,但并非总是如此。
- 类型错误的树 - 通过重新排列 typed 参数使原始类型信息无效,例如将一个论点拼接成另一个论点。这些肯定会出问题。
解决方法
希望你能看出问题是概念性的,是根深蒂固的。但是您可以采用以下两种方法之一来解决问题:
hacky 解决方案 - 通过最终结果的字符串表示进行往返:
c.parse(showCode(q"{ case $binder => $body }"))
showCode
通常会打印可解析的代码,即使untypecheck
不是幂等的。当然,这会产生一些编译时性能开销,这对于您的用例来说可能是可以接受的,也可能是不可以接受的。- 硬解 - 使用内部编译器APIs 手动检查粘合代码。我无法在一个 post 中解释如何做到这一点,但您必须学习所有关于类型、符号及其所有者的知识。最糟糕的是树是 可变的 wrt 类型信息。如果您走那条路,我建议您阅读 scala/async. 的源代码
最好的办法可能是避免编写宏,或者等到 scala.meta
的语义 API 发布,你可以将它用于 def 宏。