Template Haskell有什么不好的?

时间:2012-06-01 20:36:19

标签: haskell template-haskell

似乎Haskell社区经常将模板Haskell视为一种不幸的便利。我很难准确地说出我在这方面所观察到的内容,但请考虑这几个例子

我见过各种各样的博客文章,其中人们使用模板Haskell做了相当简洁的事情,实现了更漂亮的语法,这在常规的Haskell中是不可能的,以及巨大的样板减少。那么为什么模板Haskell以这种方式受到鄙视呢?是什么让它不受欢迎?在什么情况下应避免模板Haskell,以及为什么?

6 个答案:

答案 0 :(得分:167)

避免模板Haskell的一个原因是它整体上根本不是类型安全的,因此违背了“Haskell的精神”。以下是一些例子:

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

TH也是彻头彻尾的危险:

  • 在编译时运行的代码可以执行任意IO,包括发射导弹或窃取您的信用卡。您不希望查看您为搜索TH漏洞而下载的每个cabal包。
  • TH可以访问“模块 - 私有”功能和定义,在某些情况下完全破坏封装。

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

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

最后,有些事情使TH函数作为最终用户使用起来不那么有趣:

  • 不透明度。当TH函数具有类型Q Dec时,它可以在模块的顶层生成绝对任何内容,并且您完全无法控制将生成的内容。
  • 整体性。除非开发人员允许,否则无法控制TH函数生成多少;如果你找到一个生成数据库接口 JSON序列化接口的函数,你就不能说“不,我只想要数据库接口,谢谢;我将推出自己的JSON接口”< / LI>
  • 运行时间。 TH代码需要相对较长的时间才能运行。每次编译文件时都会重新解释代码,并且运行的TH代码通常需要加载大量的软件包。这大大减慢了编译时间。

答案 1 :(得分:50)

这完全是我自己的意见。

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

  • 写作更加丑陋。引用有时是有效的,但很多时候你必须做手动AST嫁接和管道。 API很大且很笨拙,总是有很多你不关心但仍然需要调度的情况,你关心的情况往往以多种相似但不相同的形式存在(数据vs .newtype,记录样式与普通构造函数,等等)。写作很乏味和重复,而且复杂程度不够机械。 reform proposal解决了其中一些问题(使报价更广泛适用)。

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

  • 这是没有纪律的。我的意思是,在你表达抽象的大部分时间里,抽象背后都有某种原则或概念。对于许多抽象,它们背后的原理可以用它们的类型来表达。对于类型类,您通常可以制定实例应遵守且客户可以承担的法律。如果你使用GHC的new generics feature来抽象任何数据类型(在边界内)的实例声明的形式,你可以说“对于总和类型,它的工作原理如下,对于产品类型,它的工作方式就是这样”。另一方面,模板Haskell只是宏。它不是思想层面的抽象,而是ASTs层面的抽象,它比纯文本层次的抽象更好,但只是适度的。*

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

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

  • 一般的原则是你应该使用正确的工具来完成工作,而最小的工具就足够了,而且类比模板Haskell是something like this。如果有办法做的不是模板Haskell,通常更可取。

模板Haskell的优势在于你可以用它做任何其他方式无法做到的事情,而且它是一个很大的问题。大多数情况下,TH使用的东西只有在它们直接作为编译器功能实现时才能完成。 TH非常有益,因为它可以让你做这些事情,并且因为它可以让你以更轻量级和可重复使用的方式对潜在的编译器扩展进行原型化(例如,参见各种镜头包)。

总结一下为什么我认为对模板Haskell有负面的感受:它解决了很多问题,但对于它解决的任何特定问题,感觉应该有一个更好,更优雅,更有纪律的解决方案更适合解决那个问题,一个不能通过自动生成样板来解决问题,但是不需要样板。

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

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

答案 2 :(得分:28)

我想谈谈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树的库几乎是完美的近似值。

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

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

RE Opacity:对于您调用的任何库函数,这都是相同的。您无法控制Data.List.groupBy将执行的操作。您只需要一个合理的“保证”/约定,版本号会告诉您有关兼容性的信息。这在某种程度上是一个不同的变化问题。

这是使用zeroth得到回报的地方 - 您已经对生成的文件进行了版本控制 - 因此您始终可以知道生成的代码的形式何时发生了变化。然而,对于大量生成的代码来说,看看差异可能有点粗糙,所以这是一个更好的开发人员界面将会很方便的地方。

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

答案 3 :(得分:13)

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

  
      
  • 使用起来很难看。 $(fooBar&#39;&#39; Asdf)看起来不太好看。肤浅,当然,但它有所贡献。
  •   

我同意。我觉得$()被选中看起来像是语言的一部分 - 使用熟悉的Haskell符号托盘。但是,这正是您/不希望用于宏拼接的符号中的内容。它们绝对融合得太多了,这种美容方面非常重要。我喜欢{{}}对于拼接的外观,因为它们在视觉上非常独特。

  
      
  • 写作更加丑陋。引用有时是有效的,但很多时候你必须做手动AST嫁接和管道。 [API] [1]大而且笨重,总是有很多你不关心但仍需要发送的案例,你关心的案例往往存在于多个类似的案件中但不是相同的形式(数据与新类型,记录样式与普通构造函数等)。它写起来很无聊和重复,而且很复杂,不适合机械化。 [改革提案] [2]解决了其中一些问题(使报价更广泛适用)。
  •   

然而,我也同意这一点,因为其中的一些评论是针对TH&#34;观察,缺乏良好的开箱即用AST报价并不是一个严重的缺陷。在这个WIP包中,我试图以库的形式解决这些问题:https://github.com/mgsloan/quasi-extras。到目前为止,我允许在比平常更多的位置进行拼接,并且可以在AST上进行模式匹配。

  
      
  • 舞台限制是地狱。无法拼接在同一模块中定义的函数是它的较小部分:另一个结果是,如果你有一个顶级拼接,模块中的所有内容都将超出它之前的任何范围。具有此属性(C,C ++)的其他语言通过允许您转发声明的东西使其可行,但Haskell没有。如果你需要在拼接声明或它们的依赖关系和依赖关系之间进行循环引用,那么你通常只是搞砸了。
  •   

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

  
      
  • 没有原则。我的意思是,在你表达抽象的大部分时间里,抽象背后都有某种原则或概念。对于许多抽象,它们背后的原理可以用它们的类型来表达。定义类型类时,通常可以制定实例应遵守的规则,客户可以采用这些规则。如果你使用GHC的[新泛型特征] [3]来抽象任何数据类型(在边界内)的实例声明的形式,你可以说&#34;对于和类型,它的工作原理如下,产品类型,就像那样&#34;。但模板Haskell只是愚蠢的宏。它不是思想层面的抽象,而是ASTs层面的抽象,它比纯文本层面的抽象更好,但只是适度。
  •   

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

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

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

虽然我可能错了,但从实现的角度来看,我并不认为TH是编译器扩展的复杂问题。这实际上是&#34;保持简单的好处之一&#34;没有基本层是一些理论上具有吸引力,可静态验证的模板系统。

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

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


总之,我认为TH是一个强大的,半被忽视的工具。较少的仇恨可以导致更加生动的图书馆生态系统,鼓励实施更多的语言特征原型。已经观察到TH是一种动力过大的工具,可以让你/做/几乎任何东西。无政府状态!嗯,我认为这种能力可以让你克服其大部分局限,并构建能够采用相当原理的元编程方法的系统。值得使用丑陋的黑客来模拟&#34;适当的&#34;实施,以这种方式设计&#34;适当&#34;实施将逐渐变得清晰。

在我的个人理想版本的必杀技中,大部分语言实际上会从编译器中移出,进入这些类型的库中。这些功能作为库实现的事实并没有严重影响他们忠实抽象的能力。

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

类型类让我们定义一组方法,然后可以在该类的通用函数中使用。但是,除此之外,类帮助避免样板的唯一方法是提供&#34;默认定义&#34;。现在这里是一个无原则功能的例子!

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

  • 尽管这会产生极大的便利和力量,但由于孤立实例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 hackery更可怕的符号。

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

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

答案 4 :(得分:7)

模板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甚至有效,但重点仍然存在)。

答案 5 :(得分:5)

为什么TH不好?对我而言,归结为:

  

如果您需要生成如此多的重复代码,而您发现自己尝试使用TH自动生成代码,您做错了!

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

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

(另一个原因是TH非常低级。没有宏大的高级设计;很多GHC的内部实现细节都暴露出来。这使得API容易发生变化......)