解析器恐惧

2008年5月20日

这些天我经常和人们谈论领域特定语言,我对外部 DSL 的一个常见反应是很难编写解析器。实际上,使用 XML 作为外部 DSL 的载体语法的一种理由是“你可以免费获得解析器”。这与我的经验不符——我认为解析器比大多数人想象的要容易编写,而且它们并不比解析 XML 难多少。

我甚至有证据。好吧,实际上只有一个案例,但我还是会引用它,因为它支持我的论点。当我为我的书编写入门示例时,我开发了多个外部 DSL 来填充一个简单的状态机。其中一个是使用 XML(将其作为入门药物),另一个是自定义语法,我借助Antlr对其进行了解析。编写代码来完全解析这些格式所花费的时间大致相同。

虽然你可以免费获得 XML 解析器(我使用了 Elliotte Rusty Harold 出色的XOM 框架),但 XML 解析器的输出实际上是以 XML DOM 形式呈现的解析树。为了用它做任何有用的事情,你必须进一步处理它。我对 DSL 的做法是围绕一个清晰的语义模型来构建它们,因此在这种情况下,解析的真正输出是一个填充的状态机模型。为了做到这一点,我必须编写代码来遍历 XML DOM。这并不特别困难,尤其是因为我可以使用 XPath 表达式来挑选出我感兴趣的 DOM 部分。实际上,我根本没有遍历 DOM 树——对于我感兴趣的每一件事,我都有一个方法,它会发出一个 XPath 查询,遍历结果节点并填充状态机模型。

所以 XML 处理很容易,但它并非不存在——大约一百行代码。我花了几个小时。我已经有一段时间没有使用 XOM 了,所以需要一些熟悉,但它是一个非常容易学习和使用的库。

Antlr 处理非常相似。Antlr 有一种表示法,允许你在语法文件中添加一些简单的规则来填充 AST。处理 AST 并填充语义模型的代码与 XML 代码非常相似——查询树中的正确节点,然后处理它们。包括语法文件在内,生成的代码大约有 250 行,但我花了大约相同的时间来编写它。我之前对 Antlr 很熟悉,之前用过几次,但我之前没有真正使用过树构建功能。(如果你有兴趣,可以在我的书的正在进行的工作中找到此示例的描述。)

虽然我对解析器生成器的探索让我习惯了它们比许多人想象的要容易编写,但当我意识到它实际上并不比 XML 案例慢时,我感到很惊讶。在一个更仔细控制的示例中,我仍然预计它会花费更长的时间,因为我第二个做了 Antlr 示例,正如任何程序员都知道的那样,第二种实现总是会快得多。即使如此,差异也远小于许多人似乎期望的——当“解析器”这个词似乎意味着“太复杂”时。

我不能否认,学习解析器生成器肯定存在学习曲线。你必须习惯语法文件以及它们如何与代码示例交互。你可以使用不同的策略(我目前称之为树构建、嵌入式翻译和嵌入式解释)。你还要考虑自定义语法的语法,这比考虑是否将某件事设为 XML 中的属性或元素需要更多的决策。但这曲线并不高。现代工具使它变得更加容易。Antlr 是我目前默认的选择,它带有一个非常好的 IDE,可以帮助探索语法表达式并查看它们是如何被解析成 AST 的。但是,一旦你习惯了某个解析器生成器的使用方法,学习其他解析器生成器就不难了。

那么,为什么人们对为 DSL 编写解析器存在不合理的恐惧呢?我认为这归结为两个主要原因。

  • 你在大学里没有上编译器课程,因此认为解析器很可怕。
  • 你在大学里上了编译器课程,因此确信解析器很可怕。

第一个原因很容易理解,人们天生就对他们不了解的东西感到紧张。第二个原因很有趣。这归结为人们在大学里是如何接触解析的。解析通常只在编译器课程中教授,在这种情况下,上下文是解析一个完整的通用语言。解析通用语言比解析领域特定语言要难得多,如果没有什么别的,因为语法会大得多,而且通常包含一些你可以在 DSL 中避免的讨厌的皱纹。

鼓励将解析与输出处理和代码生成交织在一起的代码加剧了这个问题。对我来说,保持事情清晰的关键是使用语义模型,这样你的解析器就不会做更多的事情,只是填充该模型。大多数情况下,我可以通过像任何其他 OO 框架一样执行该语义模型来完成我需要做的事情。大多数情况下,我不需要进行代码生成,而当我需要进行代码生成时,我将其基于语义模型,因此它独立于解析器。我认为,如果你在语法中包含了代码生成语句,那么事情就太过于耦合在一起了。

为了让人们能够有效地使用外部 DSL,他们必须以与教授解析通用语言截然不同的方式学习它。语言和语言中的脚本都很小,这改变了许多人们通常对解析的担忧。除非你真的需要,否则避免代码生成可以消除很大一部分复杂性。使用清晰的语义模型可以将步骤分离成更容易处理的块。

当然,问题在于,没有多少文献遵循这些指南。(这也是我花这么多时间研究它的原因之一。)你很难找到关于解析器生成器工具的任何文档。当你确实找到一些非常好的文档(比如 Terence Parr 的Antlr 书籍)时,它通常仍然是用通用语言思维方式编写的。不要误会我的意思,我发现 Antlr 书籍非常有用(这是 Antlr 成为我默认解析器生成器的一个重要原因),但我认为,它假设的是解析通用语言而不是领域特定语言,这使得它比本来应该的更难接近。

然而,好的一面是,你仍然可以克服学习曲线。如果你还没有尝试过使用解析器生成器,我建议你试一试。尝试编写你自己的简单 DSL。在开始时不要担心代码生成,只需像往常一样创建域模型,并让 DSL 来填充它。从一些非常愚蠢的事情开始(就像我用HelloAntlr做的那样),然后逐渐从那里开始。浏览一些使用 DSL 的开源项目,看看它们是如何做的。

我们试图做的是介绍通常用于编译器但比编译器更通用的工具,让那些将这些工具与编译器联系在一起的受众了解这些工具,因为他们一直都是这样被教导的。

-- 丽贝卡·帕森斯