Cycoe@Home

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

Author: Cycoe (cycoejoo@163.com)
Date: <2024-02-04 Sun 22:20>
Generator: Emacs 29.3 (Org mode 9.6.15)
Built: <2024-05-12 Sun 20:13>