这个答案是针对illissius提出的问题逐点回应的:
- 使用起来很丑。$(fooBar''Asdf)看起来不好看。表面上看,当然,但它确实有影响。
我同意。我觉得$( )被选择是为了看起来像语言的一部分 - 使用Haskell熟悉的符号组合。然而,这正是您不希望在宏拼接中使用的符号。它们肯定混合得太多了,这个美学方面非常重要。我喜欢{{ }}的外观,因为它们非常视觉上独特。
它的写法甚至更加丑陋。引用有时有效,但很多时候您需要手动进行AST嫁接和管道操作。 [API] [1] 太大而难以操作,总会有许多您不关心但仍需要分派的情况,并且您关心的情况往往以多个类似但不完全相同的形式存在(数据 vs。newtype,记录样式 vs。普通构造函数等)。编写这些内容是乏味且重复的,并且足够复杂以不是机械化的。[改革提案] [2] 解决了其中一些问题(使引用更广泛适用)。
我也同意这一点,然而,“TH的新方向”中的一些评论指出,缺乏良好的开箱即用的AST引用并不是一个关键性的缺陷。在这个WIP包中,我试图以库的形式解决这些问题:
https://github.com/mgsloan/quasi-extras。到目前为止,我允许在一些比平常更多的地方插入代码,并且可以对AST进行模式匹配。
舞台限制是一个大问题。无法拼接在同一模块中定义的函数只是其中的一个较小部分:另一个后果是,如果你有一个顶层splice,那么在它之后的整个模块将对它之前的任何内容都不可见。其他具有此属性的语言(如C、C++)通过允许您进行前向声明使其可行,但Haskell没有这样的功能。如果您需要在spliced声明或其依赖项和从属项之间进行循环引用,则通常会遇到困难。
我之前遇到过不可能存在循环TH定义的问题……非常烦人。有一个解决方案,但很丑陋——将涉及到循环依赖关系的内容包装在一个TH表达式中,该表达式组合了所有生成的声明。其中一个这些声明生成器可以只是一个接受Haskell代码的准引用程序。
“这是不可取的。我的意思是,当你表达一个抽象概念时,通常会有某种原则或概念支撑着这个抽象概念。对于许多抽象概念来说,它们背后的原则可以通过它们的类型来表达。当你定义一个类型类时,通常可以制定实例应该遵守的法则,客户端也可以假设这些法则成立。如果你使用 GHC 的新泛型特性将实例声明的形式抽象化到任何数据类型(在限制范围内),你可以说‘对于和类型,它的工作方式是这样的,对于积类型,它的工作方式是那样的’。但是模板 Haskell 只是愚蠢的宏,它并没有在思想层面上进行抽象,而只是在 AST 层面上进行了抽象,这比纯文本层面上的抽象好一点,但只是稍微好一点而已。”
只有当你使用不道德的方式时,它才是不道德的。唯一的区别在于,通过编译器实现的抽象机制,你更有信心抽象不会泄漏。也许民主化语言设计听起来有点可怕!TH库的创建者需要进行良好的文档记录,并清晰地定义他们所提供的工具的含义和结果。一个良好的TH原则的例子是derive包:
http://hackage.haskell.org/package/derive - 它使用DSL,以便许多派生的例子/指定/实际的派生。
它将你与GHC绑定在一起。理论上,另一个编译器可以实现它,但实际上我怀疑这将永远不会发生。(这与各种类型系统扩展形成对比,尽管它们目前可能只由GHC实现,但我可以轻松想象它们在未来被其他编译器采用并最终标准化。)
这是一个很好的观点 - TH API相当庞大且笨重。重新实现似乎会很困难。然而,表示Haskell AST的问题只有几种方式。我想复制TH ADTs,并编写一个转换器将其转换为内部AST表示,这样可以解决大部分问题。这相当于创建haskell-src-meta所需的(不可忽视的)工作量。它也可以通过对TH AST进行漂亮的打印并使用编译器的内部解析器来简单地重新实现。
尽管我可能错了,但从实现的角度来看,我认为TH并不是那么复杂的编译器扩展。这实际上是“保持简单”并且基本层不是一些理论吸引人、静态可验证的模板系统的好处之一。
API不稳定。当新的语言特性添加到GHC中,template-haskell包更新以支持它们时,这通常涉及到TH数据类型的不兼容更改。如果您希望您的TH代码与多个版本的GHC兼容,您需要非常小心,可能需要使用CPP。
这也是一个很好的观点,但有些夸张。虽然最近有一些API添加,但它们并没有造成广泛的破坏。另外,我认为通过我之前提到的优秀AST引用,实际需要使用的API可以大大减少。如果没有构建/匹配需要不同的函数,而是表示为文字,则大部分API都会消失。此外,您编写的代码将更容易移植到类似于Haskell的语言的AST表示形式中。
总之,我认为TH是一个强大但半被忽视的工具。更少的厌恶可能会导致更活跃的库生态系统,鼓励实现更多的语言特性原型。观察到TH是一个过度强大的工具,可以让你/do/几乎任何事情。混乱!我的意见是,这种力量可以让你克服它的大部分限制,并构建能够采用相当原则性元编程方法的系统。值得使用丑陋的黑客技巧来模拟“正确”实现,因为这样“正确”实现的设计将逐渐变得清晰。
在我个人理想的涅磐版本中,很多语言实际上会移出编译器,进入这些类型的库。特性作为库实现并不会严重影响它们忠实地抽象的能力。
Haskell对样板代码的典型回答是抽象。我们最喜欢的抽象是什么?函数和类型类!
类型类让我们定义一组方法,然后可以在所有泛型于该类的函数中使用。然而,除此之外,类别帮助避免样板代码的唯一方法是提供“默认定义”。现在这里有一个不合理功能的例子!
我认为对TH和类Lisp元编程的拒绝导致了对像方法默认值这样的东西的偏好,而不是更灵活的、类似宏展开的实例声明。避免可能导致意外结果的事物的纪律是明智的,然而,我们不应忽视Haskell强大的类型系统可以比许多其他环境更可靠地进行元编程(通过检查生成的代码)。