Template Haskell有什么不好的地方?


252

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

我见过很多博客文章,其中人们使用Template Haskell做得很漂亮,可以实现更漂亮的语法,这在常规Haskell中是不可能实现的,并且大大减少了样板。那么,为什么以这种方式看不起模板Haskell?是什么使它不受欢迎?在什么情况下应避免使用Haskell模板,为什么?


56
我不同意动议票;我以和我问过的精神相同的方式问这个问题。懒惰I / O有什么不好?我希望看到同样的答案。我愿意重提这个问题是否有帮助。
丹·伯顿

51
@ErikPhilips为什么不让经常使用此标签的人来确定它是否属于这里?似乎您与Haskell社区的唯一互动是放下它的问题。
加布里埃尔·冈萨雷斯

5
@GabrielGonzalez在当前的问题和答案中很明显,它没有遵循FAQ,我在这里不应该问哪种问题:没有实际的问题需要解决。该问题没有解决特定于代码的问题,本质上仅是概念性的。基于这个问题,我应该避免模板haskell,它也属于Stack Overflow而不是Recommendation Engine
Erik Philips

27
@ErikPhilips推荐引擎方面与此问题无关,因为您会注意到这是指不同工具之间的决定(例如“告诉我应该使用哪种语言”)。相比之下,我只是要特别地询问有关Template Haskell的解释,而FAQ中指出“如果您的动机是“我希望其他人向我解释[空白]”,那么您可能还可以。与之相比,例如,GOTO仍然被认为有害吗?
丹·伯顿

29
投票重新开放。仅仅因为这是一个更高层次的问题,并不意味着它不是一个好问题。
捷尔吉Andrasek

Answers:


171

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

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

TH也是完全危险的:

  • 在编译时运行的代码可以执行任意操作IO,包括发射导弹或窃取信用卡。您无需浏览所有曾经下载过的阴谋包,以搜索TH漏洞。
  • TH可以访问“模块专用”功能和定义,在某些情况下完全破坏封装。

然后,有一些问题使TH函数无法用作库开发人员:

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

最后,有些事情使TH函数作为最终用户的使用不太有趣:

  • 不透明度。当TH函数具有type时Q Dec,它绝对可以在模块的顶层生成任何东西,并且您完全无法控制将要生成的内容。
  • 整体主义。除非开发人员允许,否则您无法控制TH函数的生成量。如果找到生成数据库接口 JSON序列化接口的函数,则不能说“不,我只需要数据库接口,谢谢;我将滚动自己的JSON接口”
  • 运行。TH代码需要相对较长的时间才能运行。每次编译文件时,都会重新解释该代码,并且通常,运行中的TH代码需要大量的程序包,这些程序包必须装入。这会大大降低编译时间。

4
除此之外,从历史上来说,模板haskell的文献非常少。(尽管我只是再次看了一下,现在看来似乎至少有了一点改善。)而且,要理解模板haskell,您必须本质上理解Haskell语言语法,这会带来一定程度的复杂性(这不是Scheme)。当我还是Haskell初学者时,这两件事使我有意不去理会TH。
12

14
不要忘记,模板Haskell的使用突然意味着声明的顺序很重要!考虑到Haskell的平滑打磨,TH并没有像人们希望的那样紧密集成(可能是1.4、98年,2010年甚至是格拉斯哥)。
Thomas M. DuBuisson,2012年

13
您可以毫不费力地对Haskell进行推理,Template Haskell没有这样的保证。
2012

15
Oleg对TH的类型安全替代方案的承诺发生了什么?我指的是基于他的“最终无标签,部分评估”的论文以及他在此处的更多注释。当他们宣布它时,它看起来很有前途,然后我再也没有听到关于它的消息。
加布里埃尔·冈萨雷斯


53

这完全是我自己的看法。

  • 使用起来很丑。$(fooBar ''Asdf)只是看起来不太好。肤浅的,当然,但它有所作为。

  • 写起来更难看。有时可以进行报价,但是很多时候您必须进行手动AST嫁接和管道连接。该API庞大且笨拙,总有很多情况您不关心但仍需要分派,而您确实关心的情况往往以多种相似但不完全相同的形式出现(数据与新类型,记录-style与普通的构造函数等)。写起来很无聊而且重复,而且足够复杂以至于不能机械化。该改革方案解决了一些这个(使报价更广泛的适用)。

  • 舞台限制是地狱。无法拼接在同一模块中定义的功能只是其中的一小部分:另一个结果是,如果您具有顶级拼接,则模块中位于其后的所有内容都将超出其之前的范围。具有此属性的其他语言(C,C ++)通过允许您转发声明的内容来使其可行,而Haskell则不行。如果需要在拼接的声明或它们的依赖项和依赖项之间使用循环引用,那么通常就很麻烦。

  • 这是无纪律的。我的意思是,大多数时候表达抽象时,抽象背后都有某种原理或概念。对于许多抽象,它们背后的原理可以用它们的类型表示。对于类型类,您通常可以制定法律,实例应服从并且客户可以承担。如果您使用GHC的新泛型功能对任何数据类型(在范围内)抽象实例声明的形式,您将说“对于求和类型,它像这样工作,对于产品类型,它像那样工作”。另一方面,模板Haskell只是宏。它不是思想层面的抽象,而是AST层面的抽象,这比纯文本层面的抽象要好,但要适度。*

  • 它把您与GHC联系起来。理论上,另一个编译器可以实现它,但实际上,我怀疑这种情况是否会发生。(这与各种类型的系统扩展形成了鲜明对比,尽管它们可能目前仅由GHC实现,但我可以轻易地想象它会被其他编译器采用并最终实现标准化。)

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

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

Template Haskell的优点是您可以使用它做其他事情无法做的事情,这是一个很大的过程。在大多数情况下,仅将TH作为编译器功能直接实现时,才可以使用TH。两者兼有TH是非常有益的,因为它可以让您做这些事情,并且因为它可以使您以更轻巧和可重用的方式对潜在的编译器扩展进行原型制作(例如,请参见各种镜头包装)。

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

*尽管我经常觉得CPP它可以解决的问题具有更好的功率重量比。

编辑23-04-14:在上述内容中,我经常尝试达到的目的,直到最近才确切地了解到,抽象和重复数据删除之间存在重要的区别。正确的抽象通常会导致重复数据删除的副作用,而重复通常是抽象不足的明显迹象,但这并不是为什么它很有价值。适当的抽象是使代码正确,可理解和可维护的原因。重复数据删除只会使其更短。与一般的宏一样,模板Haskell是用于重复数据删除的工具。


“ CPP对于可以解决的问题具有更好的功率重量比”。确实。在C99中,它可以解决您想要的任何问题。考虑这些:COS混沌。我也不理解为什么人们认为AST一代更好。它只是模棱两可,并且与其他语言功能不太正交
Britton Kerin

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不透明度:这与您调用的任何库函数相同。您无法控制Data.List.groupBy的工作。您只是有一个合理的“保证” /约定,版本号告诉您有关兼容性的信息。什么时候是变化的事情。

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

RE Monolithism:您当然可以使用自己的编译时代码对TH表达式的结果进行后处理。筛选顶级声明类型/名称的代码不是很多。哎呀,您可以想象编写一个一般执行此操作的函数。对于修改/去单等化准报价器,您可以在“ QuasiQuoter”上进行模式匹配,并提取出使用的转换,或者根据旧转换进行新转换。


1
关于不透明性/单一性:当然,您可以遍历a [Dec]并删除不需要的内容,但可以说该函数在生成JSON接口时读取外部定义文件。仅仅因为您不使用它Dec,并不会使生成器停止寻找定义文件,从而使编译失败。出于这个原因,最好有一个限制性更强的Qmonad 版本,该版本可以让您生成新名称(以及类似的东西),但不允许IO,这样,正如您所说,可以过滤其结果/进行其他合成。
dflemstr 2012年

1
我同意,应该有一个非IO版本的Q /报价!这也将有助于解决-stackoverflow.com/questions/7107308/…。一旦确保了编译时代码不会做任何不安全的事情,似乎您可以对结果代码运行安全检查器,并确保它不引用私有内容。
mgsloan 2012年

1
您是否曾经提出过反对意见?仍在等待您答应的链接。对于那些正在寻找该答案的旧内容的用户:stackoverflow.com/revisions/10859441/1,对于那些可以查看已删除内容的用户:stackoverflow.com/revisions/10913718/6
Dan Burton 2012年

1
是否有比黑客版本更最新的Zeroth版本?那个(和darcs仓库)最近一次更新是在2009年。两个副本都不使用当前的ghc(7.6)构建。
aavogt

1
@aavogt tgeeky正在努力修复它,但我认为他 没有完成:github.com/technogeeky/zeroth如果没有人这样做/为此做点什么,我最终会解决的。
mgsloan 2013年

16

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

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

我同意。我觉得$()被选择为看起来像是语言的一部分-使用了熟悉的Haskell符号托盘。但是,这正是您不希望在用于宏拼接的符号中想要的。它们肯定混合得太多,并且在化妆品方面非常重要。我喜欢{{}}的外观,因为它们在视觉上非常明显。

  • 写起来更难看。有时可以进行报价,但是很多时候您必须进行手动AST嫁接和管道连接。[API] [1]大而笨拙,总有很多情况您不关心但仍然需要调度,而您确实关心的情况往往以多种相似但不相同的形式出现(数据vs. newtype,记录样式与常规构造函数等)。写起来很无聊而且重复,而且足够复杂以至于不能机械化。[改革建议] [2]解决了其中一些问题(使引用更为广泛地适用)。

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

  • 舞台限制是地狱。无法拼接在同一模块中定义的功能只是其中的一小部分:另一个结果是,如果您具有顶级拼接,则模块中位于其后的所有内容都将超出其之前的范围。具有此属性的其他语言(C,C ++)通过允许您转发声明的内容来使其可行,而Haskell则不行。如果需要在拼接的声明或它们的依赖项和依赖项之间使用循环引用,那么通常就很麻烦。

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

  • 这是无原则的。我的意思是,大多数时候表达抽象时,抽象背后都有某种原理或概念。对于许多抽象,它们背后的原理可以用它们的类型表示。定义类型类时,通常可以制定法律,实例应遵循且客户可以承担。如果您使用GHC的[新泛型功能] [3]对任何数据类型(在范围内)抽象实例声明的形式,您会说“对于求和类型,它像这样,对于产品类型,它像那样”。但是Template Haskell只是愚蠢的宏。它不是思想层面的抽象,而是AST层面的抽象,这比纯文本层面的抽象要好,但要适度。

仅当您使用它进行无原则的操作时,它才是无原则的。唯一的区别是,使用编译器实现的抽象机制,您可以更加放心抽象不会泄漏。也许使语言设计民主化听起来确实有些吓人!TH库的创建者需要很好地记录并清楚地定义他们提供的工具的含义和结果。原则上TH的一个很好的例子是派生包:http : //hackage.haskell.org/package/derive-它使用DSL,使得许多派生的示例/ specify /实际派生。

  • 它把您与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是一种功能强大的工具,可以让您/ do /几乎执行任何操作。无政府状态!好吧,我认为这种功能可以使您克服其大多数局限性,并构建能够使用具有相当原则性的元编程方法的系统。值得一提的是,使用丑陋的hack来模拟“适当”的实现,因为这样,“适当”的实现的设计将逐渐变得清晰。

在我个人理想的涅磐版本中,许多语言实际上都会从编译器中移出,进入各种库中。将功能实现为库这一事实并不会严重影响其忠实抽象的能力。

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

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

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

  • 尽管这会带来极大的便利和强大功能,但是由于孤立的实例,您无法指定超类默认值 http://lukepalmer.wordpress.com/2009/01/25/a-world-without-orphans/ 这些可以让我们修复数字层次结构优美!

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

    换句话说,它遵循TH提供的功能,但是必须将语言的整个领域(即构造语言)提升为类型系统表示形式。虽然我认为它可以很好地解决您的常见问题,但对于复杂的问题,似乎容易产生比TH骇客更可怕的符号。

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

我认为对TH和类似Lisp的元编程的拒绝导致他们倾向于方法默认值之类的东西,而不是更灵活的,宏扩展的实例声明之类的东西。避免可能导致无法预料的结果的原则是明智的,但是,我们不应该忽略Haskell的有能力类型系统比在许多其他环境中(通过检查生成的代码)允许更可靠的元编程。


4
这个答案本身并不能很好地发挥作用:您正在引用很多其他答案,在正确阅读您的答案之前,我必须先找到这些答案。
本·米尔伍德

这是真的。我试图弄清楚尽管在谈论什么。也许我会编辑以内嵌illisuis的观点。
mgsloan 2012年

我应该说,也许“无原则”是一个比我应该使用的更强的词,带有一些我不想要的含义-它当然不是不道德的!那个要点是我最烦恼的地方,因为我脑子里有这种感觉或不成熟的想法,难以用语言表达,而“无原则”是我所掌握的词之一,它在附近。“有纪律”可能更近。没有清晰简洁的表述,我去举一些例子。如果有人可以更清楚地向我解释我的意思,我将不胜感激!
glaebhoerl 2012年

(此外,我认为您可能已经调高了分期限制和丑陋的报价。)
glaebhoerl 2012年

1
h!感谢您指出了这一点!固定。是的,无纪律可能是更好的表达方式。我认为,这里的区别实际上是“内置”(因此是“易于理解”的)抽象机制与“即席”或用户定义的抽象机制之间的区别。我想我的意思是,可以想象您可以定义一个TH库,该库实现了与类型类分派非常相似的东西(尽管在编译时不是在运行时)
mgsloan 2012年

8

模板Haskell的一个相当实用的问题是它仅在GHC的字节码解释器可用时才起作用,而并非在所有体系结构上都如此。因此,如果您的程序使用模板Haskell或依赖于使用它的库,则它将无法在具有ARM,MIPS,S390或PowerPC CPU的计算机上运行。

这在实践中是相关的:git-annex是用Haskell编写的工具,可以在担心存储的计算机上运行,​​因为这些计算机通常具有非i386-CPU,这是有意义的。就个人而言,我在NSLU 2(32 MB RAM,266MHz CPU;在这种硬件上可以运行Haskell正常工作吗?)上运行git-annex。如果要使用Template Haskell,则不可能。

(有关ARM的GHC的情况现在已经大大改善了,我认为7.4.2甚至可以工作,但要点仍然存在)。


1
“模板Haskell依靠GHC的内置字节码编译器和解释器来运行拼接表达式。” - haskell.org/ghc/docs/7.6.2/html/users_guide/...
约阿希姆·布莱特纳

2
啊,我知道-不,如果没有字节码解释器,TH将无法工作,但这与ghci不同(尽管相关)。如果ghci确实取决于字节码解释器,那么ghci的可用性与字节码解释器的可用性之间存在完美的关系,我不会感到惊讶,但是问题是缺少字节码解释器,而不是缺少ghci特别。
muhmuhten

1
(顺便说一句,ghci对交互使用TH的支持很差劲ghci -XTemplateHaskell <<< '$(do Language.Haskell.TH.runIO $ (System.Random.randomIO :: IO Int) >>= print; [| 1 |] )'
muhmuhten

好的,我用ghci指的是GHC解释(而不是编译)代码的能力,而与代码是以交互方式来自ghci二进制文件还是来自TH拼接无关。
Joachim Breitner

6

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

如果您需要产生大量重复的代码,以至于试图使用TH来自动生成它,那您就错了!

想一想。Haskell吸引力的一半是它的高级设计使您可以避免必须用其他语言编写的大量无用的样板代码。如果你需要编译时代码生成,你基本上是说,无论你的语言或应用程序的设计失败你。而且我们程序员不喜欢失败。

当然有时候,这是必要的。但是有时候,您可以通过更加巧妙地设计来避免使用TH。

(另一件事是TH相当低级。没有宏伟的高级设计;公开了许多GHC的内部实现细节。这使得该API易于更改...)


我认为这并不意味着您的语言或应用程序使您失败了,特别是如果我们考虑使用QuasiQuotes。在语法方面总是要权衡取舍。一些语法可以更好地描述某个域,因此您有时希望能够切换到另一种语法。QuasiQuotes使您可以在语法之间优雅地切换。这功能非常强大,并由Yesod和其他应用程序使用。能够使用感觉像HTML的语法编写HTML生成代码是一个了不起的功能。
CoolCodeBro

@CoolCodeBro是的,准引用非常好,我想与TH稍微正交。(显然,它是在TH之上实现的。)我在考虑使用TH来生成类实例,构建多个Arities函数或类似的东西。
MathematicalOrchid
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.