265

It seems that Template Haskell is often viewed by the Haskell community as an unfortunate convenience. It's hard to put into words exactly what I have observed in this regard, but consider these few examples

I've seen various blog posts where people do pretty neat stuff with Template Haskell, enabling prettier syntax that simply wouldn't be possible in regular Haskell, as well as tremendous boilerplate reduction. So why is it that Template Haskell is looked down upon in this way? What makes it undesirable? Under what circumstances should Template Haskell be avoided, and why?

4

6 回答 6

176

避免使用 Template Haskell 的一个原因是它作为一个整体根本不是类型安全的,因此与“Haskell 精神”的大部分内容背道而驰。以下是一些例子:

  • 你无法控制一段 TH 代码会生成什么样的 Haskell AST,除了它会出现在哪里;你可以有一个 type 的值Exp,但你不知道它是否是一个表示 a[Char]或 a(a -> (forall b . b -> c))或其他的表达式。如果可以表示一个函数只能生成某种类型的表达式,或者只能生成函数声明,或者只能生成数据构造函数匹配模式等,那么 TH 会更可靠。
  • 您可以生成无法编译的表达式。foo您生成了一个引用不存在的自由变量的表达式?运气不好,您只会在实际使用代码生成器时看到这一点,并且只有在触发特定代码生成的情况下才会看到。单元测试也非常困难。

TH 也是完全危险的:

  • 在编译时运行的代码可以做任意事情IO,包括发射导弹或窃取你的信用卡。您不希望必须查看您下载的每个 cabal 软件包来搜索 TH 漏洞。
  • TH 可以访问“模块私有”函数和定义,在某些情况下完全打破封装。

然后有一些问题使 TH 函数作为库开发人员使用起来不那么有趣:

  • TH 代码并不总是可组合的。假设有人为镜头制作了一个生成器,而且通常情况下,该生成器的结构只能由“最终用户”直接调用,而不能由其他 TH 代码调用,例如一个类型构造函数列表,用于生成作为参数的镜头。在代码中生成该列表很棘手,而用户只需编写generateLenses [''Foo, ''Bar].
  • 开发人员甚至不知道可以组合 TH 代码。你知道你会写forM_ [''Foo, ''Bar] generateLens吗?Q只是一个 monad,所以你可以在它上面使用所有常用的函数。有些人不知道这一点,正因为如此,他们创建了具有相同功能的基本相同函数的多个重载版本,这些函数会导致一定的膨胀效应。此外,大多数人在Qmonad 中编写他们的生成器,即使他们不必这样做,这就像编写bla :: IO Int; bla = return 3; 您正在为函数提供比它需要的更多“环境”,并且该函数的客户端需要提供该环境作为其效果。

最后,有一些事情会让最终用户使用 TH 函数变得不那么有趣:

  • 不透明度。当 TH 函数具有 typeQ Dec时,它​​绝对可以在模块的顶层生成任何东西,并且您完全无法控制将生成什么。
  • 单体主义。除非开发人员允许,否则您无法控制 TH 函数生成多少;如果你找到一个生成数据库接口JSON 序列化接口的函数,你不能说“不,我只想要数据库接口,谢谢;我会滚动我自己的 JSON 接口”
  • 运行。TH 代码运行时间相对较长。每次编译文件时都会重新解释代码,并且通常,运行的 TH 代码需要大量的包,这些包必须被加载。这大大减慢了编译时间。
于 2012-06-01T20:57:38.203 回答
54

这只是我个人的看法。

  • 用起来很丑。$(fooBar ''Asdf)就是不好看。肤浅的,当然,但它有所贡献。

  • 写的更难看。引用有时有效,但很多时候你必须手动进行 AST 嫁接和管道。API大而笨重,总有很多你不关心但仍需要调度的案例,而你关心的案例往往以多种相似但不相同的形式存在(数据与新类型、记录-style 与普通构造函数,等等)。写起来很无聊和重复,而且复杂到不能机械化。改革提案解决了其中的一些问题(使引用更广泛适用)。

  • 舞台限制是地狱。不能拼接同一个模块中定义的函数是其中较小的一部分:另一个后果是,如果你有一个顶级拼接,模块中它之后的所有内容都将超出它之前的任何内容的范围。具有此属性的其他语言(C、C++)通过允许您转发声明事物使其变得可行,但 Haskell 不这样做。如果您需要拼接声明或其依赖项和依赖项之间的循环引用,那么您通常会被搞砸。

  • 这是没有纪律的。我的意思是,大多数时候当你表达一个抽象时,这个抽象背后都有某种原则或概念。对于许多抽象来说,它们背后的原理可以用它们的类型来表达。对于类型类,您通常可以制定实例应该遵守和客户可以假设的规律。如果您使用 GHC 的新泛型特性来抽象任何数据类型(在界限内)的实例声明的形式,您会说“对于 sum 类型,它像这样工作,对于产品类型,它像这样工作”。另一方面,模板 Haskell 只是宏。这不是想法级别的抽象,而是 AST 级别的抽象,它比纯文本级别的抽象更好,但只是适度的。*

  • 它将您与 GHC 联系在一起。理论上另一个编译器可以实现它,但在实践中我怀疑这是否会发生。(这与各种类型系统扩展形成对比,虽然它们目前可能只由 GHC 实现,但我可以很容易地想象被其他编译器采用并最终标准化。)

  • API 不稳定。当新的语言特性被添加到 GHC 并更新 template-haskell 包以支持它们时,这通常涉及对 TH 数据类型的向后不兼容的更改。如果您希望您的 TH 代码与不止一个版本的 GHC 兼容,您需要非常小心并且可能使用CPP.

  • 有一个一般原则,您应该为工作使用正确的工具,并且使用最小的就足够了,在这个类比中,Haskell 模板就是这样的。如果有一种方法不是 Template Haskell,那么它通常是可取的。

Template Haskell 的优点是你可以用它来做其他任何方式都做不到的事情,而且它是一个很大的功能。大多数情况下,使用 TH 的东西只能在直接作为编译器功能实现的情况下才能完成。TH 是非常有益的,因为它可以让您做这些事情,并且因为它可以让您以更轻量级和可重用的方式对潜在的编译器扩展进行原型设计(例如,请参阅各种镜头包)。

总结一下为什么我认为 Template Haskell 存在负面情绪:它解决了很多问题,但是对于它解决的任何给定问题,感觉应该有一个更好、更优雅、更规范的解决方案更适合解决该问题,一种不是通过自动生成样板来解决问题,而是通过消除对样板的需要

*虽然我经常觉得CPP对于它可以解决的那些问题有更好的功率重量比。

编辑 23-04-14:我在上面经常试图得到的,并且直到最近才确切得到的是,抽象和重复数据删除之间有一个重要的区别。适当的抽象通常会导致重复数据删除作为副作用,而重复通常是抽象不足的明显迹象,但这并不是它有价值的原因。适当的抽象是使代码正确、可理解和可维护的原因。重复数据删除只会使它更短。模板 Haskell 和一般的宏一样,是一种重复数据删除工具。

于 2012-06-03T15:16:49.023 回答
31

我想谈谈 dflemstr 提出的一些观点。

我不认为您不能对 TH 进行类型检查这一事实令人担忧。为什么?因为即使有错误,它仍然是编译时间。我不确定这是否加强了我的论点,但这在本质上与您在 C++ 中使用模板时收到的错误相似。我认为这些错误比 C++ 的错误更容易理解,因为您将获得生成代码的漂亮打印版本。

如果一个 TH 表达式/准引用者做了一些非常先进的事情,以至于棘手的角落可以隐藏,那么也许它是不明智的?

我最近一直在研究的准引号(使用haskell-src-exts / meta)打破了这个规则 - https://github.com/mgsloan/quasi-extras/tree/master/examples。我知道这会引入一些错误,例如无法在广义列表推导中拼接。但是,我认为http://hackage.haskell.org/trac/ghc/blog/Template%20Haskell%20Proposal中的一些想法很有可能最终会出现在编译器中。在那之前,用于将 Haskell 解析为 TH 树的库几乎是完美的近似。

关于编译速度/依赖关系,我们可以使用“zeroth”包内联生成的代码。这至少对给定库的用户来说是件好事,但对于编辑库的情况,我们不能做得更好。TH 依赖项可以使生成的二进制文件膨胀吗?我认为它遗漏了编译代码未引用的所有内容。

Haskell 模块的编译步骤的分段限制/拆分确实很糟糕。

RE Opacity:这对于您调用的任何库函数都是相同的。您无法控制 Data.List.groupBy 会做什么。您只是有一个合理的“保证”/约定,版本号会告诉您有关兼容性的一些信息。什么时候改变有点不同。

这就是使用 zeroth 获得回报的地方——您已经对生成的文件进行了版本控制——因此您将始终知道生成代码的形式何时发生了变化。但是,对于大量生成的代码来说,查看差异可能有点麻烦,所以这是一个更好的开发人员界面会派上用场的地方。

RE Monolithism:您当然可以使用自己的编译时代码对 TH 表达式的结果进行后处理。过滤顶级声明类型/名称的代码不会太多。哎呀,您可以想象编写一个通用的函数。对于修改/去单体化准引用器,您可以在“准引用器”上进行模式匹配并提取使用的转换,或者根据旧的转换创建一个新的。

于 2012-06-02T02:54:36.663 回答
15

这个答案是针对 illissius 提出的问题逐点回答的:

  • 用起来很丑。$(fooBar ''Asdf) 看起来不太好。肤浅的,当然,但它有所贡献。

我同意。我觉得 $( ) 被选择让它看起来像是语言的一部分——使用熟悉的 Haskell 符号托盘。但是,这正是您/不/想要的用于宏拼接的符号。它们肯定融合得太多了,而这个美容方面非常重要。我喜欢 {{ }} 拼接的外观,因为它们在视觉上非常不同。

  • 写的更难看。引用有时有效,但很多时候你必须手动进行 AST 嫁接和管道。[API][1] 大而笨重,总有很多你不关心但仍需要调度的案例,而你关心的案例往往以多种相似但不相同的形式存在(数据与新类型,记录样式与普通构造函数,等等)。写起来很无聊和重复,而且复杂到不能机械化。[改革提案][2] 解决了其中的一些问题(使引用更广泛适用)。

然而,我也同意这一点,正如“TH 的新方向”中的一些评论所观察到的,缺乏良好的开箱即用 AST 引用并不是一个严重的缺陷。在这个 WIP 包中,我试图以库的形式解决这些问题:https ://github.com/mgsloan/quasi-extras 。到目前为止,我允许在比平常更多的地方进行拼接,并且可以在 AST 上进行模式匹配。

  • 舞台限制是地狱。不能拼接同一个模块中定义的函数是其中较小的一部分:另一个后果是,如果你有一个顶级拼接,模块中它之后的所有内容都将超出它之前的任何内容的范围。具有此属性的其他语言(C、C++)通过允许您转发声明事物使其变得可行,但 Haskell 不这样做。如果您需要拼接声明或其依赖项和依赖项之间的循环引用,那么您通常会被搞砸。

我之前遇到过循环 TH 定义不可能的问题……这很烦人。有一个解决方案,但它很丑 - 将循环依赖中涉及的内容包装在一个 TH 表达式中,该表达式结合了所有生成的声明。这些声明生成器之一可能只是一个接受 Haskell 代码的准引用器。

  • 这是没有原则的。我的意思是,大多数时候当你表达一个抽象时,这个抽象背后都有某种原则或概念。对于许多抽象来说,它们背后的原理可以用它们的类型来表达。当你定义一个类型类时,你通常可以制定实例应该遵守和客户可以假设的规律。如果您使用 GHC 的 [新泛型特性][3] 对任何数据类型(在范围内)抽象实例声明的形式,您可以说“对于 sum 类型,它的工作方式是这样的,对于产品类型,它的工作方式是这样的”。但是 Template Haskell 只是愚蠢的宏。这不是思想层面的抽象,而是 AST 层面的抽象,它比纯文本层面的抽象更好,但只是适度的。

只有当你用它做无原则的事情时,它才是无原则的。唯一的区别是,通过编译器实现的抽象机制,您对抽象没有泄漏更有信心。也许使语言设计民主化听起来确实有点吓人!TH 库的创建者需要很好地记录并清楚地定义他们提供的工具的含义和结果。原则性 TH 的一个很好的例子是派生包:http ://hackage.haskell.org/package/derive - 它使用 DSL,因此许多派生 / 指定 / 实际派生的例子。

  • 它将您与 GHC 联系在一起。理论上另一个编译器可以实现它,但在实践中我怀疑这是否会发生。(这与各种类型系统扩展形成对比,虽然它们目前可能只由 GHC 实现,但我可以很容易地想象被其他编译器采用并最终标准化。)

这是一个很好的观点 - TH API 非常大而且笨重。重新实现它似乎很困难。然而,实际上只有几种方法可以解决表示 Haskell AST 的问题。我想复制 TH ADT 并将转换器编写为内部 AST 表示可以让您在其中获得很多好处。这相当于创建 haskell-src-meta 的(不是微不足道的)努力。它也可以通过漂亮地打印 TH AST 并使用编译器的内部解析器来简单地重新实现。

虽然我可能是错的,但从实现的角度来看,我认为 TH 并不像编译器扩展那么复杂。这实际上是“保持简单”的好处之一,而不是让基础层成为一些理论上有吸引力的、静态可验证的模板系统。

  • API 不稳定。当新的语言特性被添加到 GHC 并更新 template-haskell 包以支持它们时,这通常涉及对 TH 数据类型的向后不兼容的更改。如果您希望您的 TH 代码与不止一个版本的 GHC 兼容,您需要非常小心并且可能使用CPP.

这也是一个很好的观点,但有些戏剧化。虽然最近增加了 API,但它们并没有引起广泛的破坏。另外,我认为通过我前面提到的优越的 AST 引用,可以大大减少实际需要使用的 API。如果没有构造/匹配需要不同的函数,而是用文字表示,那么大部分 API 都会消失。此外,您编写的代码将更容易移植到类似于 Haskell 的语言的 AST 表示。


总之,我认为 TH 是一个强大的、半被忽视的工具。减少仇恨可能会导致更活跃的库生态系统,鼓励实施更多语言功能原型。据观察,TH 是一个强大的工具,它可以让你/做/几乎任何事情。无政府状态!嗯,我认为这种能力可以让你克服它的大部分限制,并构建能够采用非常有原则的元编程方法的系统。值得使用丑陋的黑客来模拟“正确”实现,因为这样“正确”实现的设计将逐渐变得清晰。

在我个人理想的涅槃版本中,大部分语言实际上会从编译器中移出,进入这些种类的库中。功能作为库实现的事实不会严重影响它们忠实抽象的能力。

Haskell 对样板代码的典型回答是什么?抽象。我们最喜欢的抽象是什么?函数和类型类!

类型类让我们定义一组方法,然后可以在该类的所有泛型函数中使用这些方法。然而,除此之外,类帮助避免样板的唯一方法是提供“默认定义”。现在这里是一个无原则的特性的例子!

  • 最小绑定集不可声明/编译器可检查。这可能导致由于相互递归而产生底部的无意定义。

  • 尽管这会产生极大的便利和强大的功能,但由于孤立实例 http://lukepalmer.wordpress.com/2009/01/25/a-world-without-orphans/ 这些将让我们修复优雅的数字层次结构!

  • 追求类似 TH 的方法默认功能导致http://www.haskell.org/haskellwiki/GHC.Generics。虽然这是很酷的东西,但我使用这些泛型调试代码的唯一经验几乎是不可能的,因为诱导的类型的大小和 ADT 与 AST 一样复杂。https://github.com/mgsloan/th-extra/commit/d7784d95d396eb3abdb409a24360beb03731c88c

    换句话说,这追求了 TH 提供的特性,但它必须将语言的整个领域,即构造语言,提升为类型系统表示。虽然我可以看到它对您的常见问题很有效,但对于复杂的问题,它似乎容易产生一堆比 TH 黑客更可怕的符号。

    TH 为您提供输出代码的值级编译时计算,而泛型则强制您将代码的模式匹配/递归部分提升到类型系统中。虽然这确实以一些相当有用的方式限制了用户,但我认为这种复杂性不值得。

我认为拒绝 TH 和类似 lisp 的元编程导致偏爱方法默认值之类的东西,而不是更灵活、宏扩展之类的实例声明。避免可能导致不可预见结果的事情的原则是明智的,但是,我们不应忽视 Haskell 强大的类型系统允许比许多其他环境更可靠的元编程(通过检查生成的代码)。

于 2012-06-06T11:55:41.937 回答
8

Template Haskell 的一个相当实用的问题是,它仅在 GHC 的字节码解释器可用时才有效,并非所有架构都如此。因此,如果您的程序使用 Template Haskell 或依赖于使用它的库,它将无法在具有 ARM、MIPS、S390 或 PowerPC CPU 的机器上运行。

这在实践中是相关的:git-annex是一个用 Haskell 编写的工具,可以在担心存储的机器上运行,这些机器通常有非 i386-CPU。就个人而言,我在NSLU 2上运行 git-annex (32 MB RAM,266MHz CPU;您知道 Haskell 在此类硬件上工作正常吗?)如果它使用 Template Haskell,这是不可能的。

(这些天,ARM 上的 GHC 情况正在改善很多,我认为 7.4.2 甚至可以工作,但重点仍然存在)。

于 2012-06-07T10:14:42.390 回答
6

为什么 TH 不好?对我来说,归结为:

如果您需要生成如此多的重复代码,以至于您发现自己试图使用 TH 来自动生成它,那么您做错了!

想想看。Haskell 的一半吸引力在于其高级设计允许您避免必须用其他语言编写的大量无用样板代码。如果您需要编译时代码生成,那么您基本上是在说您的语言或应用程序设计失败了。我们程序员不喜欢失败。

有时,当然,这是必要的。但有时你可以通过对你的设计更聪明一点来避免需要 TH。

(另外一个就是 TH 是相当低级的,没有什么宏大的高层设计,GHC 的很多内部实现细节都暴露出来了,这使得 API 很容易发生变化……)

于 2014-01-05T20:46:07.627 回答