为什么 antlr 用于模板引擎不是个好主意

原创
2019/12/23 02:05
阅读数 1.5W

    我在发布 jfinal 3.0 的时候认为 antlr 用于 "模板引擎" 并不是个好主意,两年多时间过去了,我的观点更进一步:认为 antlr 在多数 “非模板引擎” 的场景下使用也不是个好主意。

    在发布 jfinal 3.0 的时候谈到 antlr,只言片语信息量太少,引起了部分人的误解,今天就来稍稍展开聊一聊。

一、antlr 生成 Parser 难于调试、难于阅读

    首先现场直观来感受一下 jfinal 手写 Parser 与使用 antlr 生成的 parser 的对比,下面是为 jfinal enjoy 模板引擎手写的 parser:

https://gitee.com/jfinal/jfinal/blob/master/src/main/java/com/jfinal/template/stat/Parser.java

    空行 + 注释 + java 代码一共 278 行,干净利落,人类轻松阅读。更重要的是其用到的 Recursive Descent 算法简洁可靠,功能强大,随手可得。熟悉这个算法原理的同学几个小时就可以手撸一个自己的 Parser 出来。

    再来看一下 antlr 为模板引擎生成的 parse:

https://gitee.com/xiandafu/beetl/blob/master/src/main/java/org/beetl/core/parser/BeetlParser.java

    空行 + 注释 + java 代码一共 4164 行,这里一定要注意看第 3923 行的 String _serializedATN 变量,这个是其 parser 运行时所依靠的核心,人类完全无法阅读,也根本无法调试。

   这还不算完,生成这个 parser 需要先学习 antlr 的语法定义规则,然后写一个语法定义规则文件,该规则文件描述语法结构:

https://gitee.com/xiandafu/beetl/blob/master/src/main/java/org/beetl/core/parser/BeetlParser.g4

   要熟练掌握以上这套规则和语法描述,并精准无误地用于具体项目并不容易,学习成本比使用现成的 Recursive Descent 算法做的 Parser要高得多,而且生成出来的东西不可阅读、无法调试。

   那么到这里总该完事了吧?仍然没有,词法分析 lexer 还要再搞一次上面这一类的事情,学习成本再提升一倍:
https://gitee.com/xiandafu/beetl/blob/master/src/main/java/org/beetl/core/parser/BeetlLexer.g4
https://gitee.com/xiandafu/beetl/blob/master/src/main/java/org/beetl/core/parser/BeetlLexer.java
  
   特别注意看一下 BeetlLexer.java 第 156 行定义的 String _serializedATN 变量,用 antlr 生成的 lexer 同样是不可读,不可调试的。
 

     那么请问,整完 lexer 这套东西,总该完了吧?还是没有。几套规则定义文件外加两个生成的 java 文件还是跑不起来,必须要引入一个 329KB 的运行时依赖:antlr4-runtime.jar。

     就 parser 这点破事,引入一个 329KB 体量 runtime。要知道 jfinal 用这么大体量连 AOP + ORM + template engine 的事全干完了。我真不知道这个 jar 包里面在做什么?

    上述这些事,在 jfinal enjoy 模板引擎里头手撸一个 parser、lexer 完事了,现成的、成熟的算法拿来即用,parser、lexer 的原理、作用、写法在大学本科阶段是必学的,有些学校还会为此作为一个课程设计的作业。

   当你花时间学习 antlr 这套规则的时候,我早就手工撸完 parser 了。

   这里要说明一下 jfinal enjoy 独创的 DLRD 算法。jfinal enjoy 的 parser 是针对模板文件的特征,基于传统 Recursive Descent Parser 做了改进,做成了 Double Layer Recursive Descent 算法,将指令级的 parser 与表达式级的 parser 划分在两个独立的层次,属于独创。每一层与传统的 Recursive Descent 算法原理类似,对左递归、二义性等问题的处理有所改进。

    即便其他人没有 jfinal enjoy 的算法创新,仅仅使用传统的 Recursive Descent Parser,我前面的表述依然成立。

    罗总很严谨,这段的标题我用了“难于调试”、“难于阅读”,是为了不排除少数天才有这个能力,但对于绝大部分人类来说是不可调试、不可阅读的。

二、antlr 生成的 parser 对变化响应慢

  当你用 antlr 做出来的东西在语法、词法层面需要加点东西或者改点东西的时候,过程如下:
1: 修改语法、词法涉及的规则 .g4 描述文件
2: 运行 antlr 的生成器,重新生成 Parser、Lexer
3: 开始写自己的代码
    首先,注意上面的工作流程,其中前两步都是在用特定规则的描述语言在表示语法、词法,然后 antlr 将其转化成不可读、不可调试的 parser 代码。

    如果用于语法、词法的两个 .g4 规则描述文件写得有 bug,那么生成出来的 parser 就是错误的,你很难从生成的 parser 再回头去找规则描述文件中的错误和原因,因为描述文件是不能被调试的,它不是 java 代码,根本不能运行。

    其次,上面这个工作流程表明,在对词法、语法进行迭代的过程中,你要首先干很多别的事情,然后才可以开始真正写代码,这个重复、麻烦且容易出错的过程会阻碍了模板引擎的改进。

     jfinal enjoy 在迭代的过程中就改过语法,例如后来添加的 #switch 指令,enjoy 不使用 antlr 方案,基本上只需要在 Parser 中添加一个 case 分支就完事了:
   https://gitee.com/jfinal/jfinal/blob/master/src/main/java/com/jfinal/template/stat/Parser.java

    注意看第 218 行代码,根本就不需要学习、折腾 antlr 方案下的那一套东西。
  

三、至于那些用了 antlr 的项目

    首先我并不认为某些比较知名的项目用了 antlr 就证明它是个好方案,我个人的思维习惯之一就是普遍怀疑,哪怕你是权威。只有这样才有可能站在前人的肩膀上做得更好,走得更远。

   其次,antlr 被人们使用,我认为出于如下几个原因:

1: 非计算机专业的人,需要定制一个 DSL

2: 计算机专业,但大学基础没学好,不知道有现成的 parser 算法、源码可用

3: 盲目跟风的人。早期的几个知名模板引擎都用的 ANTLR、jflex、javacc 这一类,后来者跟风模仿

  主流的程序语言,如 java、C#、C++ 都是手写 parser,不可能去用 antlr 这种东西,综上所有信息量,我认为 antlr 在多数 “非模板引擎” 的场景下使用也不是个好主意。

 

    罗总在博客中谈到 “Parser, 根本就不是拿来给人读的, 也不是用来让人直接"细致打磨" 这个我完全不赞同。

   jfinal enjoy 多次新增、改进过语法,例如新增 #switch、#case 指令,再例如修正过空合并安全操作符与其它操作符混合使用时的 bug,这种情况下不可读、不可调试是绝对不行的。 

   如果 antlr 生成的 parser 出现上述类似的 bug,你无法通过调试的办法找到原因,只能硬着头皮去看那几个用文本文件书写的描述规则,该描述文件不能运行、更不能调试。

    java、C++ 这种语言为啥要自己写 parser 不使用 antlr? 新增、修改语法、处理 bug 都必须要可读、可调试

四、回到 jfinal 3.0 发布时新闻中的部分观点

    有了前面的信息量,再回看两年前我发布 jfinal 3.0 时谈到的 antlr 所用的一些词以及观点。我相信 antlr 是可靠、稳固的,但我不相信使用者制定的那套描述文件也是可靠稳固的。进而不相信生成的 parser 代码是可靠的,“飘摇不安” 就指向这里。

   antlr 那套语法描述规则并不比现成的 parser 算法容易,如果你能精准用好那套规则,早就更能用好现成的 parser 源码了,根本用不着 antlr。

 

 

展开阅读全文
加载中
打赏
107 评论
3 收藏
9
分享
返回顶部
顶部