Template Haskell 旅程 – 第二弹
1. 引用
如你所见,TH 能够生成和处理的语法树一点也不简单。同样不幸的是,可能会生成压根没法编译的 Haskell 代码。换言之,手写语法树既冗长又易出错。
幸运的是,有一种叫做引用的方法可以从任意 Haskell 代码中获取语法树。通过启用 TemplateHaskell
扩展我们将获得 5 种类型(译注:此处原文为 4 种应为笔误)的引用:
生成的东西 | 引用语法 | 类型 |
---|---|---|
声明 | [\vert{}d ... \vert{}] |
Q [Dec] |
表达式 | [\vert{}e ... \vert{}] |
Q Exp |
带类型的表达式 | [\vert{}\vert ... \vert{}\vert{}] |
Q (TExp a) |
类型 | [\vert{}t ... \vert{}] |
Q Type |
模式 | [\vert{}p ... \vert{}] |
Q Pat |
相同的代码在不同的上下文中代表不同的含义,因此我们需要这几种不同类型的引用。比如:
[e| Just x |] -- 表达式 [p| Just x |] -- 模式
<interactive>:2:12-13: error: parse error on input ‘|]’ <interactive>:3:12-13: error: parse error on input ‘|]’
其中表达式是最常用的,因此不带具体类型的引用语法 [| ... |]
等价于 [e| ... |]
[| Just x |] -- 同样是表达式
<interactive>:5:1-12: error: • Syntax error on [| Just x |] Perhaps you intended to use TemplateHaskell or TemplateHaskellQuotes • In the Template Haskell quotation [| Just x |]
引用不仅可以用于快速探索 Haskell 代码片段的表示方式,还可用于生成抽象语法树:
:{ myFunc :: Q Exp myFunc = [| \x -> x + 1 |] :}
你肯定也觉得这个版本的 myFunc
更短更易理解(译注:相较于上一篇博客中介绍的手动构造语法树的方法)。更牛逼的地方在于引用内部也可以使用接合:
:{ add2 :: Q Exp add2 = [| $myFunc . $myFunc |] :}
通过这种方式书写模板更符合编程习惯,只需要在不同代码片段中使用接合就可以改变算法。不过需要注意的是截止 GHC 8.2.2 版本,暂不支持在声明的引用中使用声明的接合。
下面我们试一下生成的 add2
函数:
runQ add2
<interactive>:7:1-4: error: Variable not in scope: runQ :: t0 -> t <interactive>:7:6-9: error: Variable not in scope: add2
2. 带类型的表达式
带类型的表达式的引用有一点特殊,它是生成 TExp a
类型的唯一方式,也就是说它是 TExp
类型的构造方法。使用带类型的表达式,编译器就能确定指定的类型与内部的类型相对应。比如,我们尝试使用引用带类型的表达式的方式重写 myFunc
函数:
:{ myFuncTyped :: Num a => Code Q (a -> a) myFuncTyped = [|| \x -> x + 1 ||] :}
译注:此处原文中的类型为
Q (TExp a)
,但在译者的 GHC9.0.2 版本的编译器中带类型的引用表达式被推断为类型Num a => Code Q (a -> a)
。Code
的类型定义为newtype Code m a = Code {examineCode :: m (TExp a)}
,可以看到新的Code