关于模板哈斯克尔有什么不好的?

Haskell社区经常将Haskell看作是一种不幸的方便。 在这方面,我很难说出我所观察到的,但考虑这几个例子

  • 模板Haskell在“The Ugly(but necessary)”中列出,以回答用户使用/避免哪些Haskell(GHC)扩展的问题?
  • 模板Haskell在newtype的值线程(库邮件列表) 的Unboxed Vectors中考虑了临时/
  • Yesod经常因为过分依赖Template Haskell而受到批评(参见博客文章以回应这种情绪)

我看过各种博客文章,人们使用Haskell模板做了非常整洁的事情,使得在Haskell中经常不可能的漂亮的语法,以及巨大的样板化。 那么为什么模板Haskell被这样看呢? 是什么让它不受欢迎? 在什么情况下应该避免模板哈斯克尔,为什么?

避免模​​板哈斯克尔的一个原因是它作为一个整体不是types安全的,因此违背了“哈斯克尔精神”的大部分。 这里有一些例子:

  • 你不能控制一个TH代码将会产生什么样的Haskell AST,除了它将出现的地方, 你可以有Exptypes的值,但是你不知道它是否是一个expression式,表示一个[Char]或者一个(a -> (forall b . b -> c))或者其他的。 如果可以expression一个函数只能生成某种types的expression式,或者只是函数声明,或者只有数据构造器匹配模式等,那么TH会更可靠。
  • 您可以生成不能编译的expression式。 你生成一个expression式,引用一个不存在的自由variablesfoo ? 运气不好,只有在实际使用代码生成器时才会看到,只有在触发特定代码生成的情况下。 unit testing也很困难。

TH也是绝对危险的:

  • 在编译时运行的代码可以执行任意IO ,包括发射导弹或窃取信用卡。 你不希望看到每一个你曾经下载过的search黑客攻击的软件包。
  • TH可以访问“模块专用”function和定义,在某些情况下完全破解封装。

然后有一些问题使THfunction作为图书馆开发人员使用起来不太方便:

  • TH代码不总是可组合的。 比方说,有人为镜头制造一个发生器,而且往往这个发生器的构造方式只能由“最终用户”直接调用,而不能由其他TH代码调用,例如,生成透镜作为参数的types构造函数列表。 在代码中生成这个列表是非常棘手的,而用户只需要编写generateLenses [''Foo, ''Bar]
  • 开发人员甚至不知道 TH代码可以组成。 你知道吗,你可以写forM_ [''Foo, ''Bar] generateLensQ只是一个monad,所以你可以使用它的所有常用function。 有些人不知道这一点,因此,他们创造了多个具有相同function的基本相同function的重载版本,这些function导致了一定的膨胀效应。 另外,大多数人在Q monad中编写他们的生成器,即使他们不需要,就像写bla :: IO Int; bla = return 3 bla :: IO Int; bla = return 3 ; 你正在给一个比它需要更多“环境”的function,并且这个function的客户需要提供那个环境作为这个function。

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

  • 不透明度。 当一个TH函数的types为Q Dec ,它可以在模块的顶层生成绝对的任何东西,而且你完全不能控制生成的东西。
  • 整体性。 除非开发人员允许,否则无法控制TH函数的产生量。 如果你find一个生成数据库接口一个JSON序列化接口的函数,你不能说“不,我只想要数据库接口,谢谢;我会推出我自己的JSON接口”
  • 运行。 TH代码需要相当长的时间才能运行。 每次编译文件时都会重新编译代码,而且运行的TH代码通常需要大量的包才能加载。 这大大减慢了编译时间。

这完全是我自己的意见。

  • 这是丑陋的使用。 $(fooBar ''Asdf)只是看起来不错。 肤浅,当然,但它有助于。

  • 写的更加丑陋。 有时引用作品,但很多时候你必须做手动AST嫁接和pipe道。 这个API非常笨重,总是有很多你不关心但是仍然需要调度的情况,你关心的情况往往会出现在多个类似但不相同的表单中(data vs. newtype,record风格与正常的构造函数等)。 写作枯燥乏味,重复性不够,不够机械。 改革提案解决了这一问题(使报价更为广泛适用)。

  • 舞台限制是地狱。 不能在同一个模块中定义的function拼接是其中的一小部分:另一个结果是,如果您有一个顶级拼接,模块之后的所有东西都将超出范围。 具有此属性的其他语言(C,C ++)通过允许您转发声明事物来使其可行,但Haskell不会。 如果您需要在拼接声明或其依赖项和依赖项之间进行循环引用,那么通常只需要拧紧即可。

  • 它没有纪律 我的意思是,当你expression一个抽象的时候,在抽象背后有一些原则或概念。 对于许多抽象,它们背后的原则可以用它们的types来expression。 对于typesclass级,您通常可以制定哪些实例应该服从和客户可以承担的法律。 如果您使用GHC的新generics特性来抽象任何数据types(在边界内)的实例声明的forms,您可以说“对于总和types,它适用于产品types,它的工作原理就像这样”。 另一方面,模板Haskell只是macros。 它不是抽象的思想层面,而是抽象层次的AST,比纯文本层面的抽象层次更好,但是只是适度的抽象。

  • 它把你和GHC联系起来。 理论上,另一个编译器可以实现它,但实际上我怀疑这是否会发生。 (这与各种types的系统扩展形成鲜明对比,尽pipe目前只能由GHC来实现,但我很容易想象到其他编译器会被采用,并最终被标准化)。

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

  • 有一个一般的原则,你应该使用正确的工具和最小的那个就足够了,在这个比喻模板Haskell是这样的 。 如果有办法做到这一点不是模板哈斯克尔,它通常是可取的。

Haskell模板的优点是你可以用它做任何事情,你不能做任何其他的事情,这是一个很大的问题。 大多数情况下,TH所使用的东西可能只能在直接作为编译器特性实现时才能完成。 TH是非常有益的,因为它可以让你做这些事情,因为它可以让你以一个更轻量和可重用的方式(例如各种镜头包)的潜在的编译器扩展原型。

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

*虽然我经常觉得CPP对于可以解决的问题有更好的权重比。

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

我想解决dflemstr带来的一些问题。

我不认为你不能担心这个事实。 为什么? 因为即使有错误,仍然是编译时间。 我不确定这是否会加强我的观点,但这与在C ++中使用模板时收到的错误在精神上是相似的。 我认为这些错误比C ++的错误更容易理解,因为你会得到一个漂亮的生成代码版本。

如果一个THexpression式/准转义者做的事情如此先进以至于棘手的angular落可以隐藏起来,那么也许这是不明智的?

我用最近一直在研究的准引用(使用haskell-src-exts / meta)来打破这个规则 – https://github.com/mgsloan/quasi-extras/tree/master/examples 。 我知道这引入了一些错误,如不能在广义列表parsing中拼接。 但是,我认为http://hackage.haskell.org/trac/ghc/blog/Template%20Haskell%20Proposal中的一些想法很可能会在编译器中结束。; 在此之前,用于parsingTH树的Haskell库几乎是一个完美的近似。

关于编译速度/依赖性,我们可以使用“零”包来内联生成的代码。 这对于给定的图书馆的用户来说至less是好的,但是对于编辑图书馆的情况我们不能做得更好。 TH依赖会膨胀生成的二进制文件吗? 我认为它遗漏了编译代码没有引用的所有东西。

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

RE不透明度:对于您调用的任何库函数,这是相同的。 你无法控制Data.List.groupBy会做什么。 你只是有一个合理的“保证”/惯例,版本号告诉你有关兼容性的东西。 这是一个不同的变化的问题时。

这就是使用第零个代价的地方 – 您已经对生成的文件进行了版本控制 – 所以您将始终知道何时生成的代码的forms发生了变化。 但是,查看差异可能有点粗糙,但对于大量生成的代码,这是一个更好的开发人员界面的便利之处。

RE Monolithism:您当然可以使用自己的编译时代码来后处理THexpression式的结果。 在顶层声明types/名称上过滤代码不会太多。 哎呀,你可以想象写一个这样做的一般function。 对于修饰/去分解的分析者,你可以在“QuasiQuoter”上进行模式匹配,并提取出所使用的变换,或者根据旧的进行一个新的变换。

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

  • 这是丑陋的使用。 $(fooBar“Asdf)只是看起来不错。 肤浅,当然,但它有助于。

我同意。 我觉得像$()被select看起来像是语言的一部分 – 使用Haskell熟悉的符号托盘。 但是,这正是你用于macros拼接的符号。 他们肯定是混在了太多,这个化妆品方面是相当重要的。 我喜欢接{{}}的外观,因为它们在视觉上截然不同。

  • 写的更加丑陋。 有时引用作品,但很多时候你必须做手动AST嫁接和pipe道。 [API] [1]非常笨重,总是有很多你不关心但是仍然需要发送的情况,你关心的情况往往存在多个相似但不相同的forms(数据与新types,logging风格与普通构造函数等)。 写作枯燥乏味,重复性不够,不够机械。 [改革build议] [2]解决了这一问题(使报价更为广泛适用)。

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

  • 舞台限制是地狱。 不能在同一个模块中定义的function拼接是其中的一小部分:另一个结果是,如果您有一个顶级拼接,模块之后的所有东西都将超出范围。 具有此属性的其他语言(C,C ++)通过允许您转发声明事物来使其可行,但Haskell不会。 如果您需要在拼接声明或其依赖项和依赖项之间进行循环引用,那么通常只需要拧紧即可。

我遇到了循环TH定义的问题,在…之前是不可能的。这很烦人。 有一个解决scheme,但它是丑陋的 – 将循环依赖关系中包含的东西包装在一个THexpression式中,该THexpression式组合了所有生成的声明。 其中一个声明生成器可能只是一个接受Haskell代码的准引号。

  • 这是无原则的。 我的意思是,当你expression一个抽象的时候,在抽象背后有一些原则或概念。 对于许多抽象,它们背后的原则可以用它们的types来expression。 当你定义一个types类时,你通常可以制定哪些实例应该服从,客户可以承担的法律。 如果您使用GHC的[新的generics特性] [3]来抽象任何数据types(范围内)的实例声明的forms,你可以说“对于总和types,它的工作原理是这样的,对于产品types,它的工作原理就像”。 但是,模板Haskell只是愚蠢的macros。 它不是思想层面的抽象概念,而是AST层次上的抽象层次,比纯文本层面上的抽象层次更好,但也只是微不足道。

如果你用它做无原则的事情,那只是无原则的。 唯一的区别是,在编译器实现了抽象机制的情况下,你更有把握地认为抽象是不漏的。 也许民主化的语言devise听起来有点可怕! TH图书馆的创build者需要很好的文档,并清楚地定义他们提供的工具的含义和结果。 原理性TH的一个很好的例子是派生包: http : //hackage.haskell.org/package/derive – 它使用一个DSL,这样许多派生/指定/实际派生的例子。

  • 它把你和GHC联系起来。 理论上,另一个编译器可以实现它,但实际上我怀疑这是否会发生。 (这与各种types的系统扩展形成鲜明对比,尽pipe目前只能由GHC来实现,但我很容易想象到其他编译器会被采用,并最终被标准化)。

这是一个非常好的点 – TH API是相当大和笨重。 重新实施它似乎可能是艰难的。 但是,实际上只有几种方法来分割代表Haskell AST的问题。 我想,复制TH ADT,并写一个转换器到内部的AST表示会给你一个很好的方式。 这相当于创buildhaskell-src-meta(不是微不足道的)。 也可以简单地通过漂亮地打印TH AST并使用编译器的内部parsing器来重新实现。

虽然我可能是错的,但从实现的angular度来看,我并不认为TH是编译器扩展的复杂。 这实际上是“保持简单”的好处之一,并且不具有理论上吸引人的静态可validation的模板系统的基础层。

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

这也是一个好点,但有点戏剧化。 虽然最近有API的join,但并没有被广泛的破坏诱导。 另外,我认为用前面提到的优越的AST引用,实际上需要使用的API可以大大减less。 如果没有构造/匹配需要不同的function,而是表示为文字,那么大部分的API就消失了。 而且,您编写的代码将更容易地移植到与Haskell类似的语言的AST表示。


总之,我认为TH是一个强大的,半被忽视的工具。 less讨厌可能导致一个更生动的图书馆生态系统,鼓励实施更多的语言function原型。 据观察,TH是一个强大的工具,可以让你/做任何事情。 无政府状态! 那么,我认为这个能力可以让你克服大部分的局限性,并且构build出能够完成原理性元编程方法的系统。 使用丑陋的黑客来模拟“适当的”实现是值得的,因为这样“适当的”实现的devise将逐渐变得清晰。

在我个人理想的涅v版本中,大部分的语言实际上都会从编译器中移出来,变成这些types的库。 function被实现为图书馆的事实并不严重影响其忠实地抽象的能力。

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

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

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

  • 尽pipe这会产生很大的便利和威力,但是由于孤儿实例,你不能指定超类的默认值。这些可以让我们修复数字层次优雅!

  • 继TH方法默认的function后,导致http://www.haskell.org/haskellwiki/GHC.Generics 。 虽然这是很酷的东西,但是我唯一的经验是使用这些generics来debugging代码几乎是不可能的,这是由于所引用的types的大小以及ADT像AST一样复杂。 https://github.com/mgsloan/th-extra/commit/d7784d95d396eb3abdb409a24360beb03731c88c

    换句话说,这是由TH提供的function,但它必须提升语言的整个领域,即build筑语言,成为一个types系统表示。 虽然我可以看到它适合你的常见问题,但对于复杂的问题,似乎很容易产生一堆比TH hackery更可怕的符号。

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

我认为拒绝TH和类似lisp的元编程会导致对方法默认等东西的偏好,而不是像实例声明那样更灵活,更macros观的扩展。 避免可能导致未预料到的结果的规律是明智的,但是,我们不应该忽视Haskell的有能力的types系统允许比许多其他环境(通过检查生成的代码)更可靠的元编程。

模板Haskell一个相当实用的问题是它只有在GHC的字节码解释器可用时才有效,在所有体系结构中情况并非如此。 所以如果你的程序使用Haskell模板,或者依赖于使用它的库,它将不能在具有ARM,MIPS,S390或PowerPC CPU的机器上运行。

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

(关于ARM上的GHC的情况很多,我认为7.4.2甚至可以工作,但点仍然是)。

为什么TH不好? 对我来说,归结到这一点:

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

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

有时候,这当然是必要的。 但有时你可以避免需要TH,只要你的devise更聪明一些。

(另一个是TH是相当低级的,没有macros伟的高层devise,很多GHC的内部实现细节都暴露了出来,这使得API很容易改变……)