问题 Haskell:模板Haskell和范围


这段代码编译得很好:

data None = None { _f :: Int }
type Simpl = Env

type Env = Int

但是,我在这段代码中出错了:

{-# LANGUAGE TemplateHaskell #-}
import Control.Lens

data None = None { _f :: Int }

type Simpl = Env

makeLenses ''None

type Env = Int

错误:

Not in scope: type constructor or class `Env'

我刚添加了一行 makeLenses ''None 类型声明之间。
这意味着TemplateHaskell代码可以改变类型构造函数的范围吗?

有谁知道这个问题的细节(或如何避免这个问题)?


948
2018-01-02 04:03


起源

这绝对是奇怪的。我查了一下 -ddump-splices 放完之后 Simpl 和 Env 彼此相邻,如果我在它们之间手动插入它就没有错误。这不是TH正在改变范围,它可能是Control.Lens.TH库中的一个错误,也可能是GHC处理TH接头的奇怪之处。 - bheklilr
用。编译 -ddump-splices 没有做任何改变,你的意思是什么 manually insert ? @bheklilr - IruT
我重新安排了定义 Simpl 和 Env 所以他们彼此相邻然后编译 -ddump-splices。然后我接受了输出并将其粘贴到定义之间的文件中 Simpl 和 Env 并删除 makeLenses ''None。它编译得很好,所以它绝对是模板haskell扩展本身就可以实现的。 - bheklilr
@bheklilr是的,没错。确实如此。谢谢你的解释:) - IruT


答案:


如果您按如下方式重新排序代码,它的工作原理如下:

{-# LANGUAGE TemplateHaskell #-}
import Control.Lens

data None = None { _f :: Int }

type Simpl = Env

type Env = Int

makeLenses ''None

当您使用Template Haskell接头为代码添加新的顶级声明时,如 makeLenses 是的,代码中的声明顺序突然变得重要!

原因是通常编译Haskell程序包括首先收集所有顶级声明并在内部对它们进行重新排序以将它们按依赖顺序排列,然后逐个编译它们(或逐个组地进行相互递归声明)。

通过运行任意代码引入新的声明,因为GHC不知道哪个声明 makeLenses 可能需要运行,而且它不知道它将产生哪些新声明。所以它不能将整个文件放在依赖顺序中,只是放弃并期望用户自己做,至少是为了决定声明是应该在拼接之前还是之后进行。

我能找到的唯一在线参考解释了这一点 原始模板Haskell纸,第7.2节,其中说该算法是:

  • 将声明分组如下:
[d1,...,da]
splice ea
[da+2,...,db]
splice eb
...
splice ez
[dz+2,...,dN]

其中唯一的拼接声明是明确指出的声明,以便每个组 [d1,...,da]等等,都是普通的Haskell声明。

  • 在第一组上执行常规依赖性分析,然后进行类型检查。它的所有自由变量都应该在范围内。

所以这里的问题是拼接之前的第一组声明在拼接之后被分别处理到第二组,并且它看不到定义 Env

我的一般经验法则是尽可能将这样的拼接放在文件的底部,但我认为这并不能保证它始终有效。


14
2018-01-02 05:44



我通常尝试在自己的模块中使用TH,除非它可以与现有声明一起线性排序。 - J. Abrahamson