Template Haskell有什么不好的?

Dan*_*ton 249 haskell template-haskell

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

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

dfl*_*str 170

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

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

TH也是完全危险的:

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

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

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

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

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

  • 奥列格对TH的类型安全替代品的承诺发生了什么变化?我指的是他的作品基于他的"最终无标记,部分评估"的论文和更多他的笔记[这里](http://okmij.org/ftp/tagless-final/index.html).当他们宣布它时看起来很有希望然后我再也没有听到过关于它的消息. (14认同)
  • 不要忘记使用模板Haskell突然意味着声明的顺序很重要!考虑到Haskell(无论是1.4,'98,2010甚至是格拉斯哥)的流畅抛光,TH都没有像人们希望的那样紧密集成. (13认同)
  • 你可以毫不费力地推理Haskell,对于模板Haskell没有这样的保证. (13认同)
  • 有[建议解决TH的一些问题](http://hackage.haskell.org/trac/ghc/blog/Template%20Haskell%20Proposal). (11认同)
  • 除此之外,模板haskell在历史上一直记录得非常糟糕.(虽然我只是再看一遍,现在似乎事情至少稍微好一些.)另外,要理解模板 - 哈希尔,你基本上必须理解Haskell语言语法,它会带来一定的复杂性(它不是Scheme).当我还是Haskell的初学者时,这两件事有助于我故意不去理解TH. (4认同)
  • re:"TH很危险,因为它可以在编译时任意IO".`cabal`钩子不能做任意的IO,与构建期间发生的事情无关吗? (4认同)
  • @Qwertie 的区别在于,受损的依赖项只会污染构建产品。TH 依赖项受损可能会将 /GHC/ 变成攻击媒介。必须担心被利用的编译器绝对是一场安全噩梦:它将每台开发人员机器和构建服务器变成了机器人。乍一看听起来还不错,因为许多组织已经将它们隔离了。但受损的开发机器仍然会造成很大的破坏。 (2认同)

gla*_*erl 51

这完全是我自己的看法.

  • 使用它很难看.$(fooBar ''Asdf)只是不好看.肤浅,当然,但它有所贡献.

  • 写作甚至更加丑陋.引用有时是有效的,但很多时候你必须做手动AST嫁接和管道.该API是大而笨重,总是有很多你不关心的案件,但仍然需要派遣,和你关心的案件往往存在于多个相似但不完全相同的形式(数据与NEWTYPE,记录-style与普通构造函数等等).写作很乏味和重复,而且复杂程度不够机械.该改革方案解决了一些这方面的(使报价更广泛的适用).

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

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

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

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

  • 一般的原则是你应该使用正确的工具来完成工作,而最小的工具就足够了,在这个类比中,模板Haskell就是这样的.如果有一种方法可以做到不是模板Haskell,那么通常更可取.

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

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

*虽然我经常觉得CPP它能解决的问题有更好的功率重量比.

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


mgs*_*oan 30

我想谈谈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"上进行模式匹配,并提取出所使用的变换,或者根据旧变换进行新变换.


mgs*_*oan 15

这个答案是为了回应illissius带来的问题,一点一点:

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

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

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

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

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

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

  • 这是没有原则的.我的意思是,在你表达抽象的大部分时间里,抽象背后都有某种原则或概念.对于许多抽象,它们背后的原理可以用它们的类型来表达.定义类型类时,通常可以制定实例应遵守的规则,客户可以采用这些规则.如果你使用GHC的[新泛型特征] [3]来抽象任何数据类型(在边界内)的实例声明的形式,你可以说"对于总和类型,它的工作原理如下,对于产品类型,它的工作原理是这样的".但模板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是编译器扩展的复杂问题.这实际上是"保持简单"的好处之一,并且没有基本层是一些理论上具有吸引力的,可静态验证的模板系统.

  • 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 hackery更可怕的符号.

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

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

  • 这个答案本身并不是很好:你在我能够正确阅读你的答案之前,已经提到了另一个我必须去找的答案. (4认同)

Joa*_*ner 8

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

  • 啊,我 - 不,没有字节码解释器,TH不会工作,但这与ghci不同(虽然相关).如果ghci的可用性与字节码解释器的可用性之间存在完美的关系,我不会感到惊讶,因为ghci确实依赖于字节码解释器,但问题是缺少字节码解释器,而不是缺少ghci特别. (2认同)

Mat*_*hid 6

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

如果你需要生成如此多的重复代码,你发现自己试图使用TH来自动生成它,那你就错了!

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

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

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