首页 文章

Template Haskell有什么不好的?

提问于
浏览
242

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

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

6 回答

  • 5

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

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

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

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

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

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

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

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

    它__37009_ t漏水了 . 也许民主化的语言设计确实听起来有点吓人! 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并且模板-haskell包更新以支持它们时,这通常涉及对TH数据类型的向后不兼容的更改 . 如果您希望TH代码不仅仅与一个版本的GHC兼容,您需要非常小心并可能使用CPP .

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


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

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

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

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

    换句话说,这是在TH提供的功能之后,但它必须将语言的整个域(构造语言)提升为类型系统表示 . 虽然我可以看到它适用于您的常见问题,但对于复杂的问题,它似乎容易产生一堆符号远比TH hackery更可怕 .

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

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

  • 28

    模板Haskell的一个相当实用的问题是它只在GHC的字节码解释器可用时才有效,而在所有架构中都不是这种情况 . 因此,如果您的程序使用Template Haskell或依赖于使用它的库,它将无法在具有ARM,MIPS,S390或PowerPC CPU的计算机上运行 .

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

    (关于ARM的GHC的情况正在改善这些天,我认为7.4.2甚至有效,但重点仍然存在) .

  • 13

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

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

    想一想 . Haskell的一半吸引力在于它的高级设计允许您避免使用其他语言编写的大量无用的样板代码 . 如果您需要编译时代码生成,则're basically saying that either your language or your application design has failed you. And we programmers don' t喜欢失败 .

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

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

  • 7

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

    • 你无法控制一段TH代码会产生什么样的Haskell AST,超出它出现的位置;您可以使用Exp类型的值,但不知道它是否是表示 [Char](a -> (forall b . b -> c)) 或其他任何内容的表达式 . 如果可以表示函数只能生成某种类型的表达式,或者只生成函数声明,或者只生成数据构造函数匹配模式等,那么TH会更可靠 .

    • 您可以生成无法编译的表达式 . 您生成了一个引用自由变量 foo 的表达式,该变量仅在实际使用您的代码生成器时才会看到,并且仅在触发生成该特定代码的情况下才会看到 . 单元测试也很困难 .

    TH也是完全危险的:

    • 在编译时运行的代码可以执行任意 IO ,包括发射导弹或窃取您的信用卡 . 您不希望查看您下载的每个cabal包以搜索TH漏洞 .

    • TH可以访问"module-private"函数和定义,在某些情况下完全破坏了封装 .

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

    • TH代码isn 't always composable. Let' s说有人为镜头制造发生器,而且通常情况下,发生器的结构将只能由"end-user,"而不是其他TH代码直接调用,例如采取用于生成镜头的类型构造函数列表作为参数 . 在代码中生成该列表很棘手,而用户只需编写 generateLenses [''Foo, ''Bar] .

    • 开发人员甚至不能_组成TH代码 . 你知道吗你可以写 forM_ [''Foo, ''Bar] generateLens 吗? Q 只是一个monad,所以你可以使用它上面的所有常用函数 . 有些人不知道这一点,正因为如此,他们创建了具有相同功能的基本相同功能的多个重载版本,并且这些功能导致一定的膨胀效应 . 此外,大多数人甚至在他们不需要时也会在 Q monad中编写他们的生成器,这就像写 bla :: IO Int; bla = return 3 ;你提供的函数比它需要的更多"environment",并且函数的客户端需要提供该环境作为其效果 .

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

    • 不透明度 . 当TH函数的类型为 Q Dec 时,它可以在模块的顶层生成绝对任何内容,并且您无法控制将生成的内容 .

    • 整体主义 . 除非开发人员允许,否则无法控制TH函数生成多少;如果你找到一个生成数据库接口的函数 and 一个JSON序列化接口,你就不能说"No, I only want the database interface, thanks; I'll roll my own JSON interface"

    • 运行时间 . TH代码需要相对较长的时间才能运行 . 每次编译文件时都会重新解释代码,并且通常需要加载运行的TH代码需要大量的软件包 . 这大大减慢了编译时间 .

  • 166

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

  • 48

    这完全是我自己的看法 .

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

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

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

    • 它's undisciplined. What I mean by this is that most of the time when you express an abstraction, there is some kind of principle or concept behind that abstraction. For many abstractions, the principle behind them can be expressed in their types. For type classes, you can often formulate laws which instances should obey and clients can assume. If you use GHC' s new generics feature用于抽象任何数据类型(在边界内)的实例声明的形式,你可以说"for sum types, it works like this, for product types, it works like that" . 另一方面,模板Haskell只是宏 . 它不是思想层面的抽象,而是ASTs层面的抽象,它比纯文本层面的抽象更好,但只是谦虚 . *

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

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

    • 一般的原则是你应该使用正确的工具来完成工作,而最小的工具就足够了,而且类比模板Haskell就是something like this . 如果's a way to do it that'不是模板Haskell,则通常更可取 .

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

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

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

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

相关问题