使用Antlr+Stringtemplate生成method chaining,一个不太简单的案例(6)
- 题外话
在森林里迷路的时候,我们希望手里有一张地图,还要有个指南针。我们心里有一个目标,要到那里去。
这是很多人首先想到的。还缺什么呢?还缺少我们当前的位置。你只有知道到自己在哪里,才能接下来的步骤。
我们要开发一个杨氏语言编译器,用 input.pipe 中的那些指令,生成C++代码。在这条路上,我们已经走了多远。
我们根据输入的格式确定了语法 pipe.g,根据输出的格式确定了模板st/header.stg,根据语法制导写翻译出了语义动作
decl.g。我们在decl.g中应用了模板。
接下来,我们需要一个东西,它能够把 调用 pipe.g 和 decl.g,并且输入文件 pipe.g 输给它们。严格的说,被调用的不是
*.g,而是antlr由 *.g 生成的词法解析、语法解析、AST遍历的java程序。
这个推动大跑的东西,可以名之为 driver,它是个java程序。
- driver java
这个程序是用于header生成的,所以我们称之为 header.java。你可能还记得,我们不只要生成头文件,还有cpp和go.cpp。
代码不复杂,但是略微有点长,我们分成三段来看。
-- 头部
我得承认,我没有命名的天份。除了头部,我还是想不出什么名字称呼这一段。在java中,它应该有专门的术语吧。
代码1:1 import java.io.*;2 import org.antlr.runtime.*;3 import
org.antlr.runtime.tree.*;
因为我们要在程序里用到这些类,所以import进来。这是常规的java写法。
题外话:有的时候,我们因为初涉足一个全新的领域,动物本能让我们保持恐惧和谨慎。在进化中,这具有优势,凡是连那是什么都不知道,就敢去碰敢吃的家伙,都年纪轻轻时候就死掉了,没有机会成为我们的祖先。所以,我们每个人的身上都保留了这样的特质。但是在学习中,有的时候,恐惧和谨慎可能过了头,阻碍我们。
我初中的时候参加数字竞赛培训。通化的初中分为山上片和山下片,山下片--我不记得那个时候的术语了--山下片的的生源较好,或者说那是富人区。我惊恐地看到老师才把题写到黑板上,有的学校的同学答案就出来了。这令我震恐。你可以想像一个非常非常难,你一辈子可能都编不出来的程序,一位大牛抽着烟喝着茶,可能还看着碟,谈笑间就写出来了。当你佩服得五体投地时,他说:没啥,就是个小小地练习。
这就是我当时的感觉。后来我看到老师写了一个式子,要因式分解的:
a^2 - b^2
全班同学瞬间就解出来啦。(a+b)(a-b)。而我完全不知道他们是怎么解出来的。我毛了,小声问旁边的同学,"这是咋整出来的啊。"如果我现在不问,老师马上就讲过去啦。
他说:这非常简单。
是的,那确实非常简单,是因式分解中最简单的公式之一,叫平方差公式,就是这个公式本身,不是灵活应用。
你明白我的意思了。恐惧,阻碍我们思考,让我们不敢假设。
其实上面的那些import就是java本身,因为我们正写的,就是java程序。纯正的,不是.g文件中的。我这么说的意思就是:.g文件的那些{}中的动作,也不过就是java程序而已,只是出现的位置略有些奇怪。如果你知道它们会在什么时候执行,就与java无异。
-- 词法和语法
接下来,我们在一个类 header 里跑 main函数。
代码2:45 public class header {6 public static void main(String
args[]) throws Exception {7 pipeLexer lex = new pipeLexer(new
ANTLRFileStream(args[0]));8 CommonTokenStream tokens = new
CommonTokenStream(lex);9 10 pipeParser parser = new
pipeParser(tokens);11 pipeParser.starting_return r =
parser.starting(); // launch parsing12 if ( r!=null )
System.out.println("parser tree:
"+((CommonTree)r.tree).toStringTree());13 14
System.out.println("---------------");15
这个main函数的前半段,如上所述。
第7行,我们构造了一个 词法分析器。
7 pipeLexer lex = new pipeLexer(new ANTLRFileStream(args[0]));
其中 pipeLexer 这个类的名字是这么来的:pipe是我们的grammar的名字,参见pipe.g(请参考昨天博客里的pipe.g源代码。);
Lexer是词法分析器的意思。
new ANTLRFileStream(args[0]) 的意思,是以此作为词法分析器的输入。
我们用这个lexer做什么呢?
8 CommonTokenStream tokens = new CommonTokenStream(lex);
我们用它作为参数,构造了一个 CommonTokenStream。token 的流。
这个流用来做什么呢?
10 pipeParser parser = new pipeParser(tokens);
我们用这个流构造了 pipeParser,这是一个
(语法的)解析器。类似pipeLexer,pipeParser的名字由两部分组成:pipe是grammar的名字,Parser是解析器。
pipeLexer,pipeParser这两个类的名字,是antlr处理pipe.g时生成的两个类。就是我这一篇博客上面提到的
"而是antlr由 *.g 生成的词法解析、语法解析"。
当终于沿 输入文件 input.pipe (即new
ANTLRFileStream(args[0]))、词法分析器pipeLexer、语法解析器pipeParser这条线走到这里,我们就可以调用语法解析器了。
11 pipeParser.starting_return r = parser.starting(); // launch parsing
我们调用了parser。调用的方法是
parser.starting()。starting()这个名字,来自我们在pipe.g中的一条规则的名字,starting。请参考昨天博客里的pipe.g源代码。
parser.starting()的返回值的类型 pipeParser.starting_return,其中starting_return
的命名,就是规则 starting 加 下划线 _,再加上 return。
以上这些命名规则,是 antlr 约定的。由antlr处理 .g 文件后,生成的lexer& parser 将遵循这样的规则,我们也遵循这样的规则来调用。
这个世界遵循两类规则。一种是强制性的。比如,如果你的C代码写得不符合C编译处标准,它就啪地给你个错误,然后甩脸子不干了。还有传说故事里的美国交警拦住你的车,要求你出示驾照,你要是敢醉么哈的冲过去,还敢动武把超啥的,他就可能会一枪把你撂倒。这是强制性的规则,有些是自然的法则,有些是人为的。
还有一种规则,是约定,即使你违反了,没有严重后果的时候似乎也没有惩罚。比如当红灯亮起,如果车辆还是强行压过斑马线,如果没有行人,也没有其他车辆,也没有摄像头和交警叔叔,那么,似乎,什么也不会发生。似乎。我们考试都做过弊,可能你没有,我有。我们口口声声说这于人无害,只要监考老师对我们仁慈一些就可以了。我们并非于人无害,这个世界上,于己有益,却于人无害的事情不多--罗素的观点,大致,你拥有很多数学知道是无害的。当我们作弊,我们无疑地伤害了没有作弊的那些同学。更严重的,我们破坏了规则。前面我说了,我也做过弊,之所以这么说的意思就是,即使我也做过,也并不意味着这件事就是正确的。
antlr的约定,大致类似于第二种。你没有遵循约定,它似乎也没有什么抱怨的。事实上,不是。它只是以另一种方式抱怨,它不工作,或者说,它不按你想像的方式工作。
当我们不认真对待代码,她也将以相同的方式回报你。君视民如草芥,民当视君如寇仇。然后我们只能感叹德国人如何如何,中国人如何如何,好像能把自己摘出去,中国人里没有你我一份似的。
如果你前面全都按 antlr 的规则,那么现在,你可以得到结果了。
12 if ( r!=null ) System.out.println("parser tree:
"+((CommonTree)r.tree).toStringTree());
那个 (CommonTree)r 里的 r,就是刚刚的规则返回值 starting_return 。它是一棵AST。为什么?因为我们在
pipe.g 里面写着 output = AST,请参见昨天博客里的 pipe.g。这不是 delc.g 里的同一条语句,还没到它。
第12行的意思是,把 pipe.g (严格地说,antlr用它生成的 lexer & parser)处理输入 input.pipe
的结果,那棵AST,转化为 toStringTree() 打印到控制台上。
我之所以写这一条语句的目的,是检查解析输入文件是否正确。
我输入了
mario:pipe_a 123 | pipe_b | pipe_c
peach:stage_1 123 | stage_2
bowser:lose_1 123 | lose_2 | lose_3 | lose_4 234
header.java运行到此处,我得到了:
parser tree: (CLASS mario (NODE pipe_a PARA 123) (NODE pipe_b)
(NODEpipe_c)) (CLASS peach (NODE stage_1 PARA 123) (NODE stage_2))
(CLASSbowser (NODE lose_1 PARA 123) (NODE lose_2) (NODE lose_3) (NODE
lose_4PARA 234))
我们看到了那些大写字母,它们是 imaginary tokens,在pipe.g中定义的。
有的同学可能发现,这里为什么没有NEXT,我们明明在 pipe.g 中定义了它,
NEXT='|';
而且,在输入文件中,我们看到了那些非常明显的 |。
因为,此处我们得到的,是 pipe.g 的输出树,而不是解析时的树。它的输出树,应用了 rewrite 规则,我们整理了这棵树,把 |
这样不携带信息的结点砍掉了。有了AST,我们可以通过结点在树中的位置确定它的语法功能,进而决定语义, | 就没有必要存在了。以下是
pipe.g 中的一段,供懒人同学们查看。我之所以没有总是贴上引用的代码,是因为那会打乱我们叙述的线索。
game : SYMBOL_NAME ':' node? ( NEXT node)* -> ^(CLASS
SYMBOL_NAME (node)*) ;
以上,我们完成了词法分析和语法解析,得到了AST。这棵抽象树,就供下面的步骤遍历,并在遍历过程中执行语义。
-- 语义
在 decl.g 中描述语义很复杂,但是调用则简单的多。
代码3:16 // walker17 try18 {19
CommonTreeNodeStream nodes = new
CommonTreeNodeStream((CommonTree)r.tree);20
nodes.setTokenStream(tokens);21 decl walker = new
decl(nodes);22 walker.starting();23 }24
catch (RecognitionException e) { 25 System.err.println(e);
26 }27 28 }29 }
我们从前往后看。
第19行,我们由AST构造出了 节点的流。
19 CommonTreeNodeStream nodes = new CommonTreeNodeStream((CommonTree)r.tree);
第20行,我们指定,这个 节点的流 里的 tokens 将使用 tokens
20 nodes.setTokenStream(tokens);
这里的 tokens,就是在代码2第8行里定义的那个。为什么需要这一步呢?
回顾代码2和代码3,我们生成这些东西的流程:
args[0](即 input.pipe) -> lex -> tokens -> parser -> r -> nodes
注意,nodes 是由 tokens 间接生成的。既然 r 是由 tokens 生成的,那么 r中原本就应该包含 token
的信息,为什么还要多余地再设置由 r 而来的 nodes的 tokenstream呢?
antlr的作者在 The Definitive ANTLR Reference 一书中这样说:
"The one key piece that is different from a usual parser and
treeparser test rig is that, in order to access the text for a tree,
youmust tell the tree node stream where it can find the token stream:"
我猜测可能在上述生成的流程中,tokens的信息被抛弃了。这一猜测是否正确,感谢哪位老师同学指点。
不过,我注释了第20行,似乎也没有什么改变,运行结果没有什么不同。也许,新的版本中,tokens信息始终携带着?
我们得到了由AST构造出的stream,接下来,我们要遍历它了,并在遍历的过程中动作。
第21行,我们用 nodes 这个 tree nodes stream 构造出一个遍历器-- walker。
21 decl walker = new decl(nodes);
你注意到了,这个 walker 的类型是 decl,这个名字从 decl.g 中的 grammar的名字而来,它被声明为 tree
grammar。请参见昨天博客中的 decl.g 。
然后,我们用这个遍历器开始:
22 walker.starting();
startinging() 的名字来自 decl.g 中的一条规则。请参见昨天博客中的decl.g 。
从 starting 开始,遍历AST,然后在遍历的过程中,执行语义动作。
有的同学可能会问,在以上java代码中,动作在哪里?动作在 decl.g 的动作部分中。当遍历 starting
这个树枝(也就是根)时,动作同时执行着。这,就是那些动作被调用的时机,解析或遍历到特定的结点,动作就开始执行。
你是不是想起了 龙书 里如何表示动作的位置。
以上,这个 header.java 调用了 *.g 产生的 *.java(里的类和方法),一边解析 input.pipe
(或遍历树),一边做着这个杨氏语言源代码要求的动作。
我们看到,一台大机器在精确地运行,输入 input.pipe 中的字符,不断地转换状态,输出 input.pipe 所规定的产品。
- 脚本,或者 调用/跑起来的 方法
调用antlr把*.g翻译为*.java,编译以上的*.java和header.java,编译并运行得到的那些c++代码,这些动作在写编译器的时候,会不断地重复。
会不断重复很多次的动作,我们应该写个程序来完成。换句话说,我们描述重复很多次的动作并命名它。
实现这个需求的最简单的工具是shell脚本。
题外话,昨天,给同学们看我写的一小段脚本,用来把一个叫做 unicode
的程序输出的东西转换为特定的格式。建一说,windows下也肯定有这样的程序,能求一个字符的 unicode 编号,弹出一个窗口……
那个弹出窗口的程序估计是存在的,它与linux下的这个程序的区别在于,linux下的这个,能用shell非常方便地取出数据,然后加工成另一种形式。易于自动化。弹出窗口那个,你如何取其中的数据呢?用hook么,是的,我们会有很多办法,但是,那是多么地不方便。因为GUI程序特意地关闭了允许你取得输出的途径,它封闭如国内的很多站点,根本就不想提供API供你调用。
张炜同学建议我贴博文的时候,同时提供主博客的URL。我还是犹豫,因为我的主博客在
blogspot,它在这个世界上是不存在的,至少在我看来。是的,我看不到我的博客。我为什么坚持使用呢?因为在那上面发贴子真的非常简单,简单到它支持向某个信箱发封信,那信的正文就是博文。
如果一个人关闭自己的心灵,不喜欢你了解他,还有什么理由抱怨大家不愿意了解和理解他呢。难道他喜欢破门而入或者喜欢各种猜测--还是他所要的不是了解和理解,而仅仅是关注。
Linux,承袭了Unix shell的血统,他一直对你张开怀抱。
代码4:1 echo cleaning2 rm -rf output && 3 rm -rf method_chaining_demo
&& 4 echo mkdir && 5 mkdir output && 6 mkdir output/classes && 7
mkdir method_chaining_demo && 8 9 echo header file generating10 echo
generating code && 11 java -cp
/home/young/Downloads/antlr-3.4-complete-no-antlrv2.jar org.antlr.Tool
-o output pipe.g decl.g && 12 echo compiling lexer and parser && 13
javac output/*.java -cp ~/Downloads/antlr-3.4-complete-no-antlrv2.jar
-d output/classes && 14 echo compiling header.java && 15 javac -cp
/home/young/Downloads/antlr-3.4-complete-no-antlrv2.jar:output/classes
header.java && 16 echo running test.java && 17 java -cp
.:/home/young/Downloads/antlr-3.4-complete-no-antlrv2.jar:output/classes
header input.pipe
第8行以前,是删除前次运行的结果,避免对本次运行造成干扰。
那些 echo 是提醒我他运行到了哪里,避免我担心。你看,他不会一直停在那不动,跟个青春期叛逆少年一样什么也不告诉你。
第11行,11 java -cp
/home/young/Downloads/antlr-3.4-complete-no-antlrv2.jar org.antlr.Tool
-o output pipe.g decl.g && 告诉 antlr 由 pipe.g 和 decl.g 两个文件,生成
*.java,放在 output 目录下。
生成了以下东西:
decl.java decl.tokens pipe.tokens pipeLexer.java pipeParser.java
你可以根据名字猜测它们的用途,相信你还看到了熟悉的面孔。
第13行,13 javac output/*.java -cp
~/Downloads/antlr-3.4-complete-no-antlrv2.jar -d output/classes &&
编译这些东西。生成一堆 .class。
我把 antlr-3.4-complete-no-antlrv2.jar 放在了 ~/Downloads/
目录下,一个糟糕的选择,它表明我没有良好的组织文件位置的习惯。
"-cp" 是做什么的?请 javac -help ,然后 RTFM。
第15行,15 javac -cp
/home/young/Downloads/antlr-3.4-complete-no-antlrv2.jar:output/classes
header.java &&
编译 header.java,我们今天写的东西。
第17行,跑。17 java -cp
.:/home/young/Downloads/antlr-3.4-complete-no-antlrv2.jar:output/classes
header input.pipe
JVM启动,许多class争先恐后装载进来,header 读入 input.pipe,然后调用那些载入的 class。大机器开动,产品在源代码的指令下生产出来。
- 后记
头文件以外,我们还需要 *.cpp 和 go.cpp 的生成,但是其余的那些,也没有什么不同。就像,当你坐上班车地铁公交,一切日子,看似没有什么不同。
当它们全部生成,我们执行:
g++ -I. *cpp -o go
*.h & *.cpp 被编译链接成了一个可执行程序 go。当我们运行go,它说:
I am mario, created in: marioI am peach, created in: peachI am bowser,
created in: bowserI am running in pipe_adata: 123I am running in
pipe_bI am running in pipe_cI am running in stage_1data: 123I am
running in stage_2I am running in lose_1data: 123I am running in
lose_2I am running in lose_3I am running in lose_4data: 234I am mario,
and game is over in: ~marioI am peach, and game is over in: ~peachI am
bowser, and game is over in: ~bowser
这像一首诗或者歌曲,让我想起另一个宣言 "I'm youth, I'm joy"。少年总会成长,承担起责任。不是保护公主,而是其他的什么人。
承担责任,也不是念两句诗,或者唱几句歌,甚至也不是声明 我愿意为你承受何种苦难。
承担责任,是虽然这些日子没有什么不同,但是如果没有你的工作,这些日子将非常不同,非常糟烂;承担责任,是拿起工具,开几亩自留地,种上土豆白菜。
这样的工具,能让你使某些人的世界不同的,有很多。其中有两种,分别叫做antlr 和 stringtemplate。
祝你开垦顺利,有收获。
Author: 杨 贵福
使用Antlr+Stringtemplate生成method chaining,一个不太简单的案例(5)
使用Antlr+Stringtemplate生成method chaining,一个不太简单的案例(5)- 一个悲惨的开始这个悲惨的开始是我今天的生活,而不是 antrl+stringtemplate。如果你从头
看到这里,最苦的地方已经过去了。昨天一点或两点睡的时候,心里想,这早晨6:40起来赶班车,对付吧。结果没睡
着,3:17,爬起来写了半张白板,又睡下,又爬起来写了不少。早晨,如果我没
记错的话,第一次,我没有听到闹表。后来复查了一下设置,没错,它响了,我
没有听到。约的是9点,8:55,孙同学电话来,她到应化所了。我当时头脑蒙
着,对话大致是这样的:啊,你去那干什么。应该在软件所啊。你回来吧。不用
急,我也得迟到。结果,迟到26分钟。屋子不大,但是有一屋子人坐好了等你。唉。我们之悲惨缘于很多因素,其中之一希望一蹴而就。我们希望开个企业,然后它
自己就能赚钱了,再也不用管;我们希望上了大学,然后就能玩了;我们希望在
什么什么之前表现得非常怎么怎么的,然后以后就可以再也不怎么怎么的了。后来发现,由于阻尼的存在,理想的无磨擦世界竟然是不存在的。我也也希望一下子就把什么学明白,或者得到什么结果。坐着抻懒腰也能减肥
啦,在家里玩也能赚钱啦,吃点什么病就好了,从此不再复发啦。而现实世界的规律有时原本就复杂,或者简单的那些规律,比如麦克斯韦四个方
程、爱因斯坦的质能方程,这些简单的规律却需要复杂的基础才能理解。我们往
往忘了,理解这些规律的复杂基础,这一工作本身也异常艰苦。编译器生成器 antlr 的使用也是如此。- 上次忘了的在语法解析中,其中有个符号 ^ ,它是AST的根。至于什么是AST,什么是
根,RTMF。我提到这个概念的动机,就是想说它时挺重要,你可能会用到。另外,我们写的语法解析的表达式,叫 EBNF,即 扩展的巴克斯-诺尔范式。编
译原理书中提到。- 语义我们用语法匹配了输入的源代码,接下来,就要在匹配的时候做点什么。这就是
语义。我们仍然以生成头文件为例。语义也写在一个.g文件里,叫decl.g,后面会和昨天我们写的 pipe.g 一起由
antlr一起生成为杨氏语言的编译器。因为anltr用于生成编译器,所以是编译器
生成器。之所以在使用语义的decl.g文件时还需要pipe.g这个语法文件,是因为我们要使
用pipe.g中的语法规则。decl.g和pipe.g共享一些语法规则,decl.g利用这些语
法规则为纲,在匹配到某个结点的时候执行特定的动作,这称为 语法制导的翻
译。语法制导,就是因为沿语法树遍历结点。这个语法树,即AST,pipe.g(严格的
说,由它生成的编译器的parser部分)的输出。我们来看decl.g的内容。-- 头部我也是词汇贫乏,想不起来什么新词,还是叫头部吧。手册里可能有专门的名字
人,但是我猜距离优雅应该同样很远。代码1:
1 tree grammar decl;
2
3 options {
4 tokenVocab=pipe;
5 output=AST;
6 ASTLabelType=CommonTree;
7 }
8
9
10 @header {
11 import org.stringtemplate.v4.*;
12 import java.util.HashMap;
13 import java.io.FileWriter;
14 }第1行,表示这是grammar,并且,这是用来遍历树的,不是lex or parser。这里的decl必须与文件同名,也就是说 tree grammar 什么东西必须放在 什么东
西.g文件 里。不然,其实也好解决,antlr会报错。上次忘了,grammar pipe 的那个文件,也同理,要叫做 pipe.g。第3行至第7行,一些配置。其中第4行,表示这一grammar将与pipe共享相同的
token。说到这里,题外话,读技术文章与小说有一处相同:如果你从中间读起,要么读
不懂,需要看前面,要么你已经看过这个故事的电影或者电视剧或者缩写版了。
还有一种可能,就是那个小说非常地好,或者非常得简单。我曾经捡到几张当时
称为大书的小说页面,看了半个下午。后来知道那是 天龙八部,萧峰用拳头慢
慢钻透墙,救段的那个场景。相信如果你看我这篇,从中间看起的话,断断不会
有那个效果,一定如坠五里雾中,除非你是来指点我的。第5行,
5 output=AST;
是很有意思的一行。它告诉antlr,我要输出一个AST。这pipe.g是一样的。后
面,我们会看到,有个东西能遍历AST。这里,因为马上就要有语义,已经是杨氏语言编译器的末端,其实也可以输出别
的东西,比如直接的结果。我们之所以选择AST的原因,请参见
[http://www.cnblogs.com/sonce/archive/2011/03/13/1982555.html],探索
Antlr(Antlr 3.0更新版)。感谢这位牛人教导我明白了AST与SAX/DOM间的类比
关系。谢谢。第10行至第14行,是因为我们的语义动作中要用到这些类,所以import进来。你
猜对了,实现动作的,就是Java语言。-- 规则,语法制导的翻译接下来的部分,就是在语法的指引下,我们来告诉antlr,遇到某些结点或者
token,我们需要做哪些特定动作。代码2:
1 starting : game+ ;这一行简单,简单到与昨天的pipe.g没有任何区别。也就是说,遇到starting的
时候,解析为+个game,然后呢,啥也不错,没有动作。接下来是规则game,这段相当之长,请有心理准备。我把它拆成了几段来介绍。--- 开始之前代码3:
1 //^(CLASS SYMBOL_NAME (node)*)
2 game
3 :
4 {
5 STGroup header = new STGroupFile("st/header.stg");
6 ST class_delc = header.getInstanceOf("class_delc");
7 }
8 ^(CLASS SYMBOL_NAME
9 {
10 class_delc.add("CLASS_NAME", $SYMBOL_NAME.text);
11 class_delc.add("CLASS_UPPER",
$SYMBOL_NAME.text.toUpperCase());
12 }第1行是用来我自己备忘的。下边的动作把语法打得七零八碎的,不然我根本记
不住自己正写的动作匹配的是哪个结点。1 //^(CLASS SYMBOL_NAME (node)*)
这一行,刚好就是pipe.g的game规则的rewrite规则。如果你还记着的话。不,
你十有八九不会记得,你得翻回昨天的博客去看pipe.g的game规则。这里,就匹配pipe.g的AST输出的东西。{} 里面的东西,就是动作;{} 以外的,就是被折散了的这个东西:
1 //^(CLASS SYMBOL_NAME (node)*)第4行至第7行意思是,在开始匹配子结点以前,初始化模板相关的东西。模板,
就是StringTemplate。5 STGroup header = new STGroupFile("st/header.stg");
6 ST class_delc = header.getInstanceOf("class_delc");第5行,从文件 st/header.stg 中载入 string template group。文件
st/header.stg 的内容和解释,请参见昨天的博客。第6行,我们要使用这个 string template group 中的 class_delc 模板。这一
模板的定义和解释,请参见昨天的博客。--- 一个简单的动作接下来,
8 ^(CLASS SYMBOL_NAME
9 {
10 class_delc.add("CLASS_NAME", $SYMBOL_NAME.text);
11 class_delc.add("CLASS_UPPER",
$SYMBOL_NAME.text.toUpperCase());
12 }这是我们遇到的第一个真正的动作,语法导制下的语义。CLASS 结点是一个imaginary结点,有印象没?参见……当我们遇到SYMBOL_NAME结点的时候,我们要执行第10行至第11行的动作。这个动作的意义是,向模板 class_delc(它是谁呢,看上面第6行)中填加变
量,这个变量的名字叫做 CLASS_NAME,它的值是$SYMBOL_NAME.text,即
SYMBOL_NAME 这个结点的文本。比如 mario。CLASS_NAME,参见昨天的博客中 header.stg 文件中的 class_delc。你是不是
把今天和昨天的博客都打开来对比着往下行进呢?我也在这么做。如果你也是的
话,请感慨一下,这个世界没有多少记忆超群的人,至少你我不是。握手。11 class_delc.add("CLASS_UPPER", $SYMBOL_NAME.text.toUpperCase());这行就简单了,我们要再加入一个变量--你可能已经找到了,模板class_delc有
三个参数,还有一个在后面--这第二个变量是CLASS_UPPER,值是
$SYMBOL_NAME.text.toUpperCase()。$SYMBOL_NAME.text是一个String,所以toUpperCase()可以RTFM
[http://docs.oracle.com/javase/1.5.0/docs/api/java/lang/String.html#toUpperCase()]
。有的同学已经想到了,有时还可以把这个text转成int,转成float。此外,有的同学可能也注意到了,加入变量的时候,我们总是关注两个东西:变
量的名字,变量的值,这就像map,键 和 值。以上,就是语义动作。没有看到输出?因为我们的动作,就是把一些变量放到模
板里,后面统一渲染。没错,还是这个小资词汇,render。那个时候,我们就得
到了真正的输出。我们为什么不直接输出,而要使用stringtemplate这么个间接的东西呢?跟我们
不用CGI perl的道理是一样的,因为很多要输出的东西,是固定的,而要填充的
东西,那些占位符,只是其中的少数。我们不想把固定的东西放在程序的逻辑中
输出。--- 一个复杂的动作,带有聚合的,应用模板于变量之上接下来,我们遇到了一个复杂的语义动作。代码4:
13 (node
14 {
15 HashMap mf = new HashMap();
16 mf.put("class_name", $SYMBOL_NAME.text);
17 mf.put("function_name", $node.node_name);
18 mf.put("para_name", $node.para_name);
19 class_delc.add("member_function_list", mf);
20 }
21 )*)看第19行,我们也是加入了一个变量,名为 member_function_list 。也许你还
记得,这本身就是一个模板(函数),我们要 apply that template on 这个传
入的变量的值上。那个函数只有一个参数,就是mf;而在那个函数中,我引用了
这个参数的成员。代码4.1
1 member_function (mf) ::= <<
2 $mf.class_name$* $mf.function_name$($mf.para_name$);
3 >>第15行,我们建立一个HashMap,它有来形成聚合(aggregation),也就是说,
传一个对象,就是mf,进去。我们看到,这个对象有三个成员,
16 mf.put("class_name", $SYMBOL_NAME.text);
17 mf.put("function_name", $node.node_name);
18 mf.put("para_name", $node.para_name);
刚好与代码4.1,也就是header.stg中的函数里引用的变量的成员对应。19 class_delc.add("member_function_list", mf);
我们把做好的这个对象做为变量加进去。以上,是一个复杂的动作,带有聚合的,需要应用模板(函数)的。--- 渲染当我们把模板中所有的变量都赋了值,就可以渲染模板了。代码5:
22 {
23 String result = class_delc.render();
24 System.out.println(result);
25
26 try{
27 FileWriter fw = new
FileWriter("method_chaining_demo/"+$SYMBOL_NAME.text+".h");
28 fw.write(result);
29 fw.flush();
30 }
31 catch (java.io.IOException e)
32 {
33 System.err.println(e);
34 }
35 }
36 ;第23行,渲染模板。渲染这个词挺优雅的,实质就是得到一个字符串--把模板里
的占位符,那些洞,都用变量填上,然后把模板作为字符串返回来。第24行,把这个字符串输出到控制台。第26至第35行,是把这个字符串输出到磁盘文件中,并以
$SYMBOL_NAME.text+".h" 作为文件名。这里的$SYMBOL_NAME,根据语法
1 //^(CLASS SYMBOL_NAME (node)*)
正是 类名mario。之所以要操作文件的原因,是因为我还要生成 cpp,要生成go.cpp(driver),并
且不希望自动为输出文件命名,而不希望由 go.sh 负责命名。我们以上为模板中的占位符赋值了变量。事实上,即使不对任何变量赋值,也可
以渲染模板。stringtempalte会认为那些变量都是null,直接跳过,输出占位符
没有被代换的模板。--- 语义动作的返回值在 antlr 中,动作还可以有返回值。我们在上面的代码6的第17行和第18行,引
用过node规则的返回值。17 mf.put("function_name", $node.node_name);
18 mf.put("para_name", $node.para_name);接下来,是node规则的动作。代码6:
1 //^(NODE SYMBOL_NAME (PARA INT)?)
2 node
3 returns [String node_name, String para_name]
4 @init {
5 $node_name = "";
6 $para_name = "";
7 }
8 :
9 ^(NODE SYMBOL_NAME (PARA INT
10 {
11 $para_name = "int par";
12 }
13 )?)
14 {
15 $node_name=$SYMBOL_NAME.text;
16 }
17 ;第3行,表示 返回值分别为 String node_name, String para_name。当在上一
级规则中引用的时候,我们就使用 $node.node_name.text,
$node.para_name.text 这样的形式。是的, antlr的规则可以有多个返回值,这与C/C++不太一样。第4行至第7行,是初始化部分,在匹配这条规则之前执行。这与前面代码3中的第
4行至第7行的不同之处在于,代码3是执行于某个 alternative(若干个由 | 分
隔开的规则匹配"路径") 之前,而这里的初始化,是在整个规则所有的
alternative之前。我们在初始化部分中把要返回值赋值为空串了。也可以赋值为报错信息,如果在
语义动作中没有正确赋值,就报错。9 ^(NODE SYMBOL_NAME (PARA INT
10 {
11 $para_name = "int par";
12 }
13 )?)第9行至第13行的动作,是当 (PARA INT)? 存在的时候执行的,因为我们把动作
放在了这个位置: (PARA INT 这个位置 )?这完成了一个逻辑判断,即 只有当参数 PARA INT 存在的时候,才会对
$para_name 赋值。这样,当输入的杨氏语言源代码中有参数时,函数的声明就有
参数;当源代码中没有参数时,$para_name 就是空串,模板渲染以后,在函数
的参数列表里什么也没有。14 {
15 $node_name=$SYMBOL_NAME.text;
16 }这个动作,不同于带?的部分,只要匹配了 node 这条规则,就一定会在最后执
行--为 $node_name 赋值。这就是目标代码中的函数名。今天又整了这么多,相信你也累了。明天继续,将介绍header.java将如何调用
lexer, paser, and tree walker,也将介绍脚本如何编译和运行一切。附录 以上涉及到的源代码1. st/header.stgdelimiters "$", "$"class_delc(CLASS_UPPER, CLASS_NAME, member_function_list) ::= <<
#ifndef _$CLASS_UPPER$_H_
#define _$CLASS_UPPER$_H_#include <iostream>class $CLASS_NAME$
{
private:
int data; public:
$member_function_list:member_function(); separator="n"$
$CLASS_NAME$();
~$CLASS_NAME$();
};
pics
使用Antlr+Stringtemplate生成method chaining,一个不太简单的案例(4)
使用Antlr+Stringtemplate生成method chaining,一个不太简单的案例(4)- 题外话昨天看到一个笑话。有个人在论坛上问,为什么车总是不走直路呢。后面一堆问
细节的。楼主又跑上来说,难道不是方向盘上的那个横档是平的,车就应该一直
向前走么。以方向盘判定车应该走直线,如果方向盘和车轮都不偏的话,那应该是正确的一
种渠道,而且比用眼睛看车轮更间接,也更便利一些。不过,再后面回贴的人提
到:应该眼前向前看,向远处看。换句话说,以车走直线作为车走直线的标准。写程序也是一样,"难道不应该是...",我们就是以输出结果为准的。现实世界
也是一样,到底牛顿对爱因斯坦对还是玻尔对,判定的标准再直接不过,以事实
为准。至于说事实如何判定,那就是更深入的另一个问题了。想起这个笑话,是因为今天安装宜家风格的柜子。宜家风格,就是一堆板子和一
堆螺丝,你自己把它们装配起来。我发现自己看不清螺丝顶部和螺丝刀结合的地
方。用手摸着对上,卡住--对了,螺丝刀能卡住的地方,就是结合正确的位置。马克思说:实践是检验真理的唯一标准。近年来,小资老资们对各种东西都开始质疑了。不过,在工程中,实践仍然是检
验真理的唯一标准。车怎么才能开得直呢,当你检验结果是车走直线的时候,你
开得就直了。当我们生成的代码与我们期望生成的代码一致时,我们就成功了。其他的无论什
么,权威、领导、老师、教科书,说你对了,都不一定是正确的。有人可能问,
为什么我制造与我期待的一致,但是却没有达到我想要的效果呢--比如我们的
method chaining 代码可能编译失败。其实原因很简单的,你制造的与你期待的一致,但是那却不是你想要的。换句话
说,你还不知道你想要的是什么。且慢哭泣,你的努力也没有白费,因为你至少知道了,这,不是你想要的。你可能还想继续问:你想要的是什么。孩子,如果你自己都不知道,我又怎么会
知道呢?解决方法有很多,但是并非你喜欢的。我当年剧烈头疼的时候发现,喝了酒头就
不疼了;当年抑郁的时候,发现喝了咖啡心情就舒畅多了。所以,当我头疼当我
抑郁的时候,我的解决手段就是喝酒喝咖啡。你该问了,如果一直一直头疼一直
一直抑郁,怎么办呢?那就一直一直喝酒一直一直喝咖啡啊。你可能还会追问,
那得到啥时候是个头啊。某关同学(不是小关,是韩师姐夫)这样解答:喝咖啡
能预防心脏病,为啥哩,因为它能让心脏PENGPENG跳。有人问,那心脏的寿命岂
不是要受到影响?是啊。但是,心脏通常能比人活得更长久,也就是说,你会因
为别的毛病死掉,在这种情况下,为什么还要担心心脏呢。这也是一种解决之道,当然,对于希望 *一劳永逸* 地解决问题的同学,不太对
胃口。不过,让我告诉你一个事实,一劳永逸根本就是骗局,请回想你高中老师
是怎么对你描述美好的大学生活的。我们还是继续来看这个简单的问题,antlr+stringtemplate 使用吧。我们按这样的顺序来介绍:语法,模板,语义,脚本,或者 调用/跑起来的 方
法。因为语法规定了输入,模板规定了输出,这两个更简单和易于观察验证;语
义规定了如何把输入翻译为输出;脚本规定如何把上述这些东西整到一起跑起来。我们仍然先讨论头文件 .h 的生成。- 语法回顾,我们的输入是这样的:代码1:
1 mario:
2 pipe_a 123 | pipe_b | pipe_c
3
4 peach:
5 stage_1 123 | stage_2
6
7 bowser:
8 lose_1 123 | lose_2 | lose_3 | lose_4 234它重复了很多次类的声明,类里面有几个方法。这些方法的调用顺序在当前的问
题头文件 .h中是不需要考虑的。我们把语法在 pipe.g 中规定,这个文件由四部分组成。我们依次来看。1. grammar这部分非常短,是这样的,只有一行:代码2:
1 grammar pipe;上次我们提到,pipe.g 将由antlr处理,生成一些java源代码,我们把它们叫做
parser们。这些parser用于完成语法解析。这行代码的意思就是告诉 antlr ,我
要生成一个这个东西。有的同学可能会问,.g文件们本来就是用来描述语法(grammar,又译作文
法)的,为什么还要特别指出要生成它呢,难道还能生成别的么。是的,antlr能指定 词法lexer, 语法parser, treeparser,和混合的这几种grammar。我
们这里指定的是混合的,既有lerxer,也有parser。有了这条指令,antlr就将试图把下面的东西作为grammar规定来看待,生成我们
指定的lexer+parser。2. 头部(?)信息我不知道应该怎么称呼这一部分,各种选项什么的。如果不指出选项的细节,选
项二字也没有什么意义,我们直接来看细节吧。代码3:
1 options {
2 output = AST;
3 ASTLabelType=CommonTree;
4 language = Java;
5 }
6
7 tokens {
8 NEXT='|';
9 CLASS;
10 NODE;
11 PARA;
12 }第1行和第7行,就那么写,分别代表它们英文本身的含义。token是个好玩的词,
极有历史,指法老手里的权杖。学习网络的同学也会觉得它面熟,令牌环网
IEEE802.5。它旁证了计算机专业的大师们是多么地没有文化,好不容易找到个
好词,到处用啊。不像人文类的,连 现如今,为了强调与众不同,都要重新起
个名字,叫做当下。当下啊当下,立马就小资情怀出众了。不是么?你把token
改成更有文化的词试试,antlr立马翻脸不认你,"滚犊子,能不能说人话?"。第2行,表示我们要把输入变成啥东西,不是变成输出,那还得在挺后面模板那
里才能涉及到。在这一步,我们把输入变成 AST,抽象语法树。如果简单理解AST,可以想像成语法描述的手段,在AST的结点里,存储着从输入
中不同位置剥离出来的信息--要创造的类的名字啦,它的方法都叫什么名字啦,
有没有参数啦。这些东西,都挂在AST的结点里,所以当你想办法遍历这棵树的
时候,你就看到了那些信息。关于AST,建议参考两份资料。一份是本书,《编译原理》,随便哪本都有,《龙
书》最佳。另一份是一篇文章,伟大的七格同学的作品《语法树》,是一篇极其
光辉灿烂摇曳多姿的小说。即然输出类型需要指出可能是AST,当然就还可以是别的什么。如果感兴趣,请
参考antlr手册,或者作者的两本书。官方网站上有提到。第3行,ASTLabelType=CommonTree; 意思是生成CommonTree,当然也可能是别
的。别的,请参考手册,同上。以后这个请参考手册,同上,就不写全了,我们
简写为 RTFM。这个词不是我杜撰的,其含义请google。第4行,language = Java; 表示目标语言,就是那些parser们的java代码的语
言是java。你猜对了,还可以是别的语言,比如C,C++啥的。RTFM。关于这个词
的使用,请参见上一段。以上,RTFM,这个词在某处定义了,然后到处使用,正是编程的核心思想之一,
重用。编译器的存在,其意义也在于此。3. parser这一部分就是语法解析的核心了。代码4:
1 starting
2 : game+
3 ;
4
5 game
6 : SYMBOL_NAME ':' node? ( NEXT node)*
7 -> ^(CLASS SYMBOL_NAME (node)*)
8 ;
9
10 node
11 : SYMBOL_NAME INT? -> ^(NODE SYMBOL_NAME (PARA INT)?)
12 ;上述代码4,就是对杨氏语言的语法描述。第1行至第3行,表示:杨氏语言的源文件是由很多个叫做 game的结点组成的。
有多个少这样的结点呢,+,这个符号的意思是 1 个或者更多。还有些别的符
号,*啊,?啊什么的,RTFM。加号个game形成了一个叫starting的节点,后面我们解析的时候,就要告诉
header.java从这里开始动手。第2行的冒号和第3行的分号,就这么写。很多个game组成starting,那么game是什么呢?第5行至第8行回答了这个问题。5 game
6 : SYMBOL_NAME ':' node? ( NEXT node)*
7 -> ^(CLASS SYMBOL_NAME (node)*)
8 ;第6行表示:每个game在输入里,都是应该是这样的,先是一个SYMBOL_NAME,然
后跟一个冒号(输入里没引号的),然后是一个叫做node的结点(?表示它可能
存在也可能不存在);接下来是一堆东西 ( NEXT node)* ,*个(即0个或者更
多)NEXT node,其中的node和上述node是同一个东西。有人说,停,SYMBOL_NAME和NEXT和node都是什么呢?类似于game,后面有定义。
我们一会再谈这个。继续看,第7行有个有意思的东西。
7 -> ^(CLASS SYMBOL_NAME (node)*)->,叫做 rewrite,有译作重写。->后面的东西,是我们要把输入变成什么样的
语法树传到输出里。估计你还记得,pipe.g的输出不是最终输出的C++代码,
而是AST。->就规定了这个输出的AST与输入(的语法树)间的对应关系。为什么要rewrite呢?一个原因是我们希望在后继的解析和语义过程中,语法树能
以一种更方便我们(杨氏语言编译器程序员)一些,而不是更方便盟友(杨氏语
言源代码程序员)。我们在->之前的语法,是为了盟友提供服务的,要尽可能让
他们用起来方便,就是 input.pipe 的样子;这里,通过 -> 改变成方便我们工
作的形式。有时,我们还可以舍弃一些没用的节点,或者添上一些 虚的
(imaginary)结点。第7行中的CLASS就是一个虚的节点,有时候需要用虚结点来
区别语法树相同的规则--即两条规则都使用了相同的语法树。对了,补充,类似第1第至第3行,或者类似第5行至第8行,这样的条目,我们称
为规则(rule)。4. lexer上面提到,还有些东西没有定义。比如SYMBOL_NAME和NEXT和node。node已经在
parser部分第10行定义了,与前两条规则没啥区别。SYMBOL_NAME和NEXT不太一样,一个曲型的特征就是它们是全大写的。它们放在
lexer部分,称为token。代码5:
1 SYMBOL_NAME
2 : ('A'..'Z'|'a'..'z'|'_') ('A'..'Z'|'a'..'z'|'_'|'0'..'9')*
3 ;
4
5 WS
6 : (' '|'t'|'n'|'r')+ {$channel = HIDDEN;}
7 ;
8
9 INT
10 : ('0'..'9')+
11 ;第1行至第3行表示:定义SYMBOL_NAME。SYMBOL_NAME是大小写字母或下划线开头,
后面接*个大小写字母或下划线或数字。就是C语言变量或函数名(合称symbol
name)的规范。第5行至第7行,定义了要跳过的符号,空格,tab什么的。第9行至第11行,定义了parser部分引用的一个token,整形数据INT。为了简单,我
们的目标代码,如果有参数,就只传int的,且只有一个。语法(parser)和词法(lexer)看起来差不多。因为一些机制上的不同,所以
要分开对待。细节,RTFM。- 模板接下来我们针对输出的结果写一个模板文件。我放在了工作目录下的st目录下,
头文件生成要用的模板是 header.stg。.stg只有三部分,在简单的案例中,甚至可以紧缩成一部分。1. 头部。头部这个名字也是我瞎起的,不知道手册里叫做什么。代码6:
1 delimiters "$", "$"也只有一行。告诉stringtemplate,不是告诉antlr,我们要用$作为开始一个占
位符的标志,也用$作为结束一个占位符的标志。占位符这个词我们此前提到过,要
准备用一个变量去填充的东西。2. 正文如果紧缩为一个部分,这部分是必须有的。它规定了我们打算输出什么样的东
西,架子,以及放在架子某个位置的占位符。代码7:
1 class_delc(CLASS_UPPER, CLASS_NAME, member_function_list) ::= <<
2 #ifndef _$CLASS_UPPER$_H_
3 #define _$CLASS_UPPER$_H_
4
5 #include <iostream>
6
7 class $CLASS_NAME$
8 {
9 private:
10 int data;
11
12 public:
13 $member_function_list:member_function(); separator="n"$
14 $CLASS_NAME$();
15 ~$CLASS_NAME$();
16 };
17
18
19
使用Antlr+Stringtemplate生成method chaining,一个不太简单的案例(3)
使用Antlr+Stringtemplate生成method chaining,一个不太简单的案例(3)- 前面忘了交待的事之所以要写这篇博客的原因,是因为在网上看到的 antlr 教程大部分都是进行四
则运算。四则运算的确很经典,不过对于学生来说,有另一个例子比只有一个例
子更好。前几天查别的资料的时候,我抱怨过到处都是同一个贴子,赵秋实同学:那你就
自己写一个吧。写一个挺累的,但是他说的对,所以我就写了一个不是四则运算
的。- 开发工具及版本上次提到要生成的东西,现在终于说到正题,生成这些产品的工具,代码生成我
们的开发工具版本是antlr-3.4-complete-no-antlrv2.jar。为了方便看语法树和
简单调试,你还可以下载 antlrworks-1.4.3.jar。这两个东西都可以从
[http://antlr.org/download.html]下载。其中已经包含stringtemplate了,不
必另外下载。此外,antlr需要java运行时库,目前的版本要求是1.5或更高。为了跑我们生成的代码,还需要g++。我用的版本是 g++ (Ubuntu
4.4.3-4ubuntu5) 4.4.3。因为生成的代码涉及规范都很基本,理论上,你用啥版
本都行。我在Linnux下跑所有这些东西,Ubuntu。因为这些东西都是跨平台的,你用什么
操作系统都应该可以。不过,请原谅我给的运行脚本--相当于批处理,只有
Linux版本。脚本只是为了运行方便,即使你不懂脚本,根据我的解释,手动重现
或编个批处理应该都不是难事。- 生成仅有工具而没有操纵工具的灵魂,是无法赋予工具以智慧的。所以,我们需要一
些文件,用以指导 antlr+stringtemplate 工作。我们一共要生成三种东西:.h, .cpp. go.cpp(driver),还要再写两个脚本,一
个用于调用生成的过程,另一个用于编译和执行生成的产品。为了生成这三种产品,我们需要以下文件,作为指导 antlr+stringtemplate 运
行的指令。这几个文件,除了扩展名,可以认为都是瞎起的,只是为了方便我们
记忆,没有别的意义。以下,以生成头文件为例说明。除了pipe.g和go.sh,头文件、cpp文件、go.cpp
每个目标都需要一组以下这些东西。1. pipe.g:.g表示这是grammar,文法文件。这里,存有我们要实现的杨氏语言的
语法。语法,在自然语言中,是规定一个句子的主谓宾和时态等如何表达的规则。现在
的中国人,都很熟悉英语的语法。你没看错,是中国人,熟悉的是英语的语法,
另一个国家的。其实汉语本身也是有语法的,只是当今有些年轻人不再知道了。当年我们语文课中学到:主谓宾定状补,这都是句子的成份。还在偏正短语啥的。
当然,与英语不同(这也谈不上特殊,请不要走到另一个极端,和咱们类似的也
有的是),汉语使用助词而不是动词的变形表示时态。你吃了吗 和 你吃吗 的区别就在于时态。汉语和英语,都是 主+谓+宾 这种形式。而日文(还有德文?)就是 主 + 宾 +
谓 这种形式。位置、变形等决定了词的语法意义。比如我们的输入文件:mario:
pipe_a 123 | pipe_b | pipe_c其中的 "mario:" 表示打算建个类,名字叫做 mario。"pipe_a 123 | pipe_b | pipe_c"表示打算在这个类里建三个方法,其中pipe_a
有个参数。调用的时候传参123进去,调用的顺序依次是 pipe_a, pipe_b,
pipe_c。这些打算,就是语法(syntax)告诉我们的。根据语法判断输入文件
input.pipe,即杨氏语言的源文件打算做什么,这个过程叫做解析(parser)。2. decl.g:.g表示这是grammar,确切地说,是tree grammar文件。我们使用了
AST(抽象语法树)来帮助实现pipe.g的语法中指定的那些"打算"。我们把为了实现这些打算而写的代码,称为动作(action)。打比方来说。一句话,对于源代码而言,通常是一个命令,比如"吃饭"。这是个
祈使句。通听懂"吃饭",分解为 动词吃 和 宾语名词饭,这就是语法分析。用什么样的动
作才能实现吃这个动作,怎么把饭作为动词吃执行的对象,这就是动作需要指定
的。杨氏语言的编译器,就像一台机器,语法指定了它能听懂你的指令,而动作
规定了它执行这些命令的措施--移动哪个肢张开多大口--包括这些针对不同规格
的饭的行为。我们把这些动作--针对语法而执行的语义(semantics)。3. header.java语法和语义文件,会经antlr处理生成几个java类,这些类的调用是由
header.java完成的。header.java这个名字也基本是随意起的,之所以称为
header是因为要用于生成头文件,之所以.java,那是因为它就真的是个java源
文件,后来会被编译为.class。这个比较简单,基本是套路的,抄来改吧改吧就能用。4. input.pipe就是它:mario:
pipe_a 123 | pipe_b | pipe_cpeach:
stage_1 123 | stage_2bowser:
lose_1 123 | lose_2 | lose_3 | lose_4 234我们的杨氏语言编译器读了这个输入的杨氏语言源代码以后,会生成三个类,分
别是mario,即马利,peach,那个公主,还有bowser,乌龟壳boss。它们分别有
两三四个类,如上所示。估计你完全能看懂,这比C++简单多了。5. go.sh这个是脚本,是用来调用header.java的,及一些前期处理工作,删除以前的生成
结果啦,建个工作目录啦,编译那些java文件(antlr从我们.g里生成出来的,还
有header.java)啦啥的。对了,它还会把 input.pipe 作为 杨氏语言编译器的
输入,并且编译杨氏语言编译器输出的C++代码,然后执行一下。脚本的动机是,我改一下.g文件,然后运行一次go.sh,就能自动地把上述工作完
成。顺便说一句为什么非得有个自动的东西,而不是手动执行那几行命令--因为
真的要执行非常非常多次。.g文件,也就是整个事情的核心,非常地不容易写。
非常不容易写的原因,如某位外国友人程序员说的:antlr的报错,也就是对.g处
理的报错信息,就像加过密一样难读。我们之所以还要容忍它的原因,是因为同时它也真的很富有生产力。6. header.stg:String Template Group,模板(组)。header.stg源代码中有的地方看起来像这样:#ifndef _$CLASS_UPPER$_H_
#define _$CLASS_UPPER$_H_对比一下我们要生成的头文件#ifndef _MARIO_H_
#define _MARIO_H_是不是觉得似曾相识?我们要做的,就是要在.g里把模板载入,然后用从源代码 input.pipe 中解析出
来的 mario 再改成大写,用来代替 CLASS_UPPER,即两个$中间的内容。当然,实际要替换的东西比这要复杂,尤其是当模板中的某一区域要重复很多
次,而次数和内容取决于源代码 input.pipe 的时候。我们一共就要生成这些东西。它们之间的关系,也就是整个系统跑起来的原理是:下面的 "->" 表示数据流,而不是谁变成了谁。有括号"()"的节点,表示程序,
没 "()" 的,表示数据。1. pipe.g + header.g -> (antlr) -> 一些java文件--parser们2. input.pipe -> (header.java 调用 parser们) -> .cpp和.h们 + go.cpp3. go.sh 调用以上过程。
shell助我去战斗
shell助我去战斗客户催进度的消息传来几次,我恨恨地想:再催再催,我就把你们全带去封闭开
发。好吃好喝,不让睡觉,天天干活。我估算了一下,担保不等项目结束,客户全过劳死光,我一定还能幸存。编码间歇看邮件,ZHUMAO同学带来噩耗,他要暂时关闭我们的GIT服务器,得备
份。不少项目都有单儿的 git repository,在ZHUMAO的服务器上中央存储。关键是,不
少,我得把它们全pull一次,有些不紧急的,我已经一段时间没pull了。恩。背影知识:git是一种版本控制系统,版本控制就是你写一大篇文章,中间
的很多过程都要记录下来,留着有用。pull,就是把git服务器上的东西整下来。一个个项目。一个个进入那些目录,pull,然后再进入下一个目录。非常不好。那么,编段程序完成这个任务吧。编程序完成的好处在于:虽然花了时间,可能是等长甚至更长的时间,却可以更
容易保证质量。这好比你打算用蜡烛做十个小兔子,如果一个个用手雕刻,那个每个的质量都是
无关的。如果花时间做个模具,那么,只要有一个小兔子是质量过关的(并且工
艺上,比如温度,控制得当),那么其他所有的小兔子就都是质量过关的。1. 统一规格我找到所有的配置文件,把 remote 和 url (就是远程的git服务及指定上面的
项目)都改成相同的。当然,可以编个awk&sed程序直接改了,但是我没那个功力,且学会了"对付一下
得了,不要么通用"的原则。所以,我找到每一个 不符合 要求的配置文件,然
后手动修改。这么找:: find . -name config | xargs grep -i ".231" -nH-nH参数的作用是打印是哪个文件的哪行命中了。2. 写个程序,遍历目录,挨个pull就这样:1 for i in $(ls -d */)
2 do
3 echo pulling $i
4 cd $i
5 convmv * -r -f utf-8 -t gb2312 --notest > /dev/null 2>&1
6 git pull origin master
7 convmv * -r -f gb2312 -t utf-8 --notest > /dev/null 2>&1
8 cd ..
9 done啥意思呢?第1行,for是个循环,它将令变量i遍历 $() 里的东西,即 ls -d */。ls -d */ 的作用是列出当前目录下所有的目录。第2行至第9行,是循环体。在每次循环时,都完成以下任务。1. 第3行,显示当前正pull谁呢,让我心里有个数。不然,通常我就等不及强制
结束了。2. 第4行,进入以$i这个变量为名的目录。3. 第5行和第7行,是把文件名在utf-8和gb2312间编码转换。原因是:a.祖国尚
未统一世界,万码奔腾的局面还会存在很多年;b.虽然通知了所有的程序员不要
使用中文文件名,大家还是有时候会忘。解释一下"> /dev/null 2>&1"的意思。> /dev/null 是把输出重定向到黑洞里
去,免得烦我。你肯定也有那感觉,多余的信息,不如没有。2>&1 的意思是把错
误信息,即出错时的报错信息也扔到黑洞里去。你看,我连错误也不想看到...跟
我们中的某些人类似。4. 第6行,是核心,也就是 git pull origin master,从origin指向的url中
pull下东西,放到master分支中去。为了核心的,我们真正要做的事,我们做了多少准备工作啊。而且,还有收尾工
作。5. 第8行,收尾,回到上一级目录,以便继续遍历下一个项目。故事再次上演,
没有一点改变。这个故事告诉我们,真正的我们想做的事,往往在一大堆看似无关和无聊的事情
当中。我们需要发现本质,同时,我们也需要有能力完成核心以外的事情。这个故事还告诉我们,第8行,收尾工作非常重要。项目的开始并非开始于项目开
始的时候,而是开始于上一个项目结束的时候。所以,令我们安慰的,如果当前有什么事情无法完成,那不是此刻的你的错误,
而是更早的你积累的结果。非常简单的推论,如果未来的你有什么事情无法完
成,其错肇始于此刻。
使用Antlr+Stringtemplate生成method chaining,一个不太简单的案例(2)
使用Antlr+Stringtemplate生成method chaining,一个不太简单的案例(2)前传,母模 method chaining我们要用 antlr+stringtemplate 开发的东西,是 method chaining 的生成器。
因此,我们得先知道 method chaining 个什么样子。正如当我们想做个蜡烛小东
西的时候,我们得先做出这个小东西的母模来。母模与最终的批量产品看起来样子完全相同,但是,母模是用手工打造的。method chaining 是实现 Fluent interface 的一种手段,这俩在 wikiepdia
上都是条目,详情请自己去查。Fluent interface 是软件工程中的一种方法,希望能让代码更可读。试对比:方案A:o_mario->pipe_a(123);
o_mario->pipe_b();
o_mario->pipe_c();方案B:o_mario->pipe_a(123)->pipe_b()->pipe_c();或者方案C:o_mario->pipe_a(123)
->pipe_b()
->pipe_c();是不是觉得方案B和方案C的可读性更好一些?况且,我们少键入几次o_mario,
也减少了错误的可能。Fluent interface 的提出者是牛人 Martin Fowler。如果你读书不注意作者的
话,可能会不记得他。他的作品包括 分析模式(不是四人帮的设计模式),UML
精粹,重构,Domain-Specific Languages 等。他是敏捷方法、极限编程、UML
和模式领域的专家,还推动了 依赖注入(控制反转)一词的流行 。以上八卦结束,无外乎想说明 method chaining 这技术系出名门,有纯正的民间
血统。Fluent interface 能够让调用这一方法的程序员写出的代码,看起来像是一种
领域定义语言,更专门,也更容易被本领域的专家(或人类)识读。也就是说,
读者可以经受更少的训练即可读懂。凡是具有平易近人特性的东西,似乎也都具有另一个特性,那就是精美的包装。
而包装是需要代价的。不过,我们遵循这样的原则:当你是一个库函数的程序员的时候,库函数的接
口,应该以令使用它的人感到愉悦为原则。你在此时多花费的时间,不仅楚人失
之楚人得之,而且将以十倍百倍在别人那里得到节省。这也正是我们的价值所在。那种认为"我依赖你对你撒娇,那都是看得起你"的
人,可能从来没想过它的反命题,"我不允许你依赖不允许你撒娇,那都是看得起
你",因为把你视为平等的人类而不是低一等级的什么。工程中没有小情小调。你真的以为现在撒娇的你,将来遇到问题的时候能够替大
家挡下风雨么?所以,如果我们是库函数程序员,我们要提供令别人觉得享受的接口去调用。
method chaining就令人愉悦,所以我们要考虑一下怎么实现了。我们的盟友需要的效果是这样的:o_mario->pipe_a(123)->pipe_b()->pipe_c();那么,我们考虑一下实现。1. o_mario->pipe_a(123),第一个函数的调用o_mario(严格的说,是这个指针类型的变量的类类型)需要有一个方法,这
个方法是pipe_a.这个容易,就是这样:代码1:class mario
{
public:
pipe_a(int par);
};以上是头文件中的内容,函数的实现非常简单,先不讨论.2. o_mario->pipe_a(123)->pipe_b(),第二个函数的调用,及第一个函数的声明后面这个pipe_b()是个什么东西呢?这应该是另一个方法。谁的方法呢。从库函数使用者的角度看,它应该是
o_mario->pipe_a(123) 这段代码的返回值 的成员函数。这里有两件重要的事。第一,重申:它应该是o_mario->pipe_a(123) 这段代码的
返回值 的成员函数。因为pipe_b()的前面有 ->,所以无疑的,前面是个类的实
例的指针。看不懂的同学,请回去复习一下C语言中的structure的成员变量如何
引用 和 这个structure的指针如何引用成员变量。第二,一种思维方法。当我们讨化如何实现的时候,我们可以从接口(广义的)
的形势入手,而不从对实现的猜测本身入手。也就是说,我们先明确它应该是个
什么样子,而不是应该如何实现。因为在这里,接口,是动机,要首先明确,而
实现手段,可以有千千万万,我们一个个穷举起来代价比较大。以上两件重要的事讨论完毕,我们再回头来看,这句话有点长:o_mario->pipe_a(123)->pipe_b()中的pipe_b()是 o_mario->pipe_a(123) 这段
代码的返回值 的成员函数。即 第二个函数,是第一个函数的返回值的成员。理解了这一点以后,我们可以再进入一步问,我们需要的是第一个函数返回值的
成员,那么,第一个函数的返回值是什么东西么?它应该是一个类类型(class type)实例的指针。哪一个类类型为好呢? class mario 就非常适合。所以,我们把代码1改一下,确定第一个函数的返回值类型,变成下面这样:代码2:
class mario
{
public:
mario* pipe_a(int par);
};以上是这个函数的"接口",即怎么去调用它。3. 第一个函数的实现我们需要的已经明确,下一步才是如何得到。在这里,我还是想再一次强调,明确自己需要的是什么非常重要,远远比知道如
何去实现要重要。始终坚守并提醒自己想要的是什么,希望能有效地避免走上与
自己的愿望相反的道路。WG同学提到过一个问题,如果把一群人关起来,不让他们了解外部的世界,给他
们好吃好喝,或者教育他们认为自己得到的是最好的,这是否给了他们幸福。这个问题可以用另外的两个似乎无关的事情来回答。一是,有日本人称中国人为支那人,我们很不喜欢,认为受到了侮辱。有人提到
过,那不就是个名字么,为什么认定这是侮辱。提这个问题的人显然没有意识到
这样一个道理:当你认定自己受到了侮辱的时候,那么就是受到了侮辱。这与发
出行为的人的动机甚至关系都不那么紧密。所谓尊重,就是不做对方认为侮辱的
事情。所以前面提到的类似观点"我欺负你正是爱你",可以问问对方,他喜欢这样的爱
么。这类似于百年前的男人问问自己的老婆,我殴打你才是爱你,你接受么?对方不接受的,就不是他想要的。当我们讨论对方的感受时,我们必须讨论对方
的感受,而不是你的感受。你没看错,我说的就是"当我们讨论对方的感受时,我
们必须讨论对方的感受",即,当我们讨论A时,我们必须讨论A。道理朴素到可以归结为 A就是A,但是有些人仍然不懂,因为他们加了很多附加
条件,却无视这些条件都与A无关。WG同学的方案里,如果被关起来的那些人觉得好,那就是好呗。问题是,那些人
*真的*觉得好么?家长包办婚姻(这个词对你来说,是不是史前时代的古拉丁语)的时候对孩子
说,"我都是为了你好啊"。且住,那得由你来判定。第二个回答WG同学的例子。扯淡的。我喜欢一个人,我把他,对不起,我把她捆
起来,不放走,每天喂猴头燕窝鲨鱼翅。最后她终于逃脱,找来一群人要揍我。我可不可以说:至少,你吃到并消化了那么多好东西,那都是我卖血换来的啊。
你如何报偿我呢。这个例子如此浅显,以至于你都愤慨了吧。但是它和WG同学那个看似合理的方案
有一个共同点,即 被捆起来饲养的那位,她愿意吗。愿意,意愿,希望得到的是什么,这非常重要。这比如何实现重要一百倍。以上扯淡结束。我们已知库函数使用者想要的是 代码2。其实,至此我们还没有
做一点自己的库函数开发的工作,只是明确了盟友的需求。应该是这样实现的。代码3,加了行号。1 mario* mario::pipe_a(int par)
2 {
3 std::cout << "I am running in " << __FUNCTION__ << std::endl;
4 this->data = par;
5 std::cout << "data: " << data << std::endl;
6 return this;
7 }其中,第1行就是声明的重复。第3行和第5行,是为了调试的时候方便,能看着点啥。__FUNCTION__ 是编译成
debug版内置的,函数名。第4行,是为了显示效果,有一个成员变量,data,int型
的。第6行是有意思的一行,它的作用,即我们刚刚讨论的,我们的盟友需要的,返
回值。返回值是什么呢?是this指针。它是一个指针,指向了这个类类型的这一个实例。所以,pipe_a 和 pipe_b 是同一个实例(的指针)调用的成员函数,这些方法
的数据将存储在(或读取自)同一个实例中。也正因为同一实例这一点,经常有
人用 method chaining 来初始化类类型的变量。比如这样;
代码4,出自[http://en.wikipedia.org/wiki/Fluent_interface#C.2B.2B] FluentGlutApp(argc, argv)
.withDoubleBuffer().withRGBA().withAlpha().withDepth()
.at(200, 200).across(500, 500)
.named("My OpenGL/GLUT App")
.create();4. o_mario->pipe_a(123)->pipe_b()->pipe_c()我们目前实现到 pipe_b(),容易看出,后面的 pipe_c() 与 pipe_b() 没有区
别。这样,代码5:mario* mario::pipe_c()
{
std::cout << "I am running in " << __FUNCTION__ << std::endl;
return this;
}5. 调用者我们替盟友写一段代码,测试一下是否能工作,然后再交付。交付的时候才发现
很多麻烦没有解决,无论是多小的麻烦,都应该认识到,那是我们的责任,不能
推给库函数使用者去完成,因为他不替你领工资。我们把这段调用库函数的代码称为 driver,推动事情运行的东西。代码6:#include <mario.h>
int main(int argc, char *argv[])
{
mario* o_mario = new mario();
o_mario->pipe_a(123)->pipe_b()->pipe_c();
delete o_mario;
return 0;
}6. 对生成 method chaining 的展望这样,我们需要三个文件:mario.h 头文件,类及其成员的声明;
mario.cpp cpp文件,类及其成员的定义;
go.cpp driver文件,负责调用mario的函数们,即
o_mario->pipe_a(123)->pipe_b()->pipe_c()。7. 重提母模如果我们仅只需要马利这样一个类,那么手写就可以了,手写完交给库函数程序
员成千上万次像driver那样调用.但是我们可能需要很多个这样的类,而它们的结构如此类似。所以,我们需要大
量地成批地生成它们。对于每一个类,我们都需要 .h和.cpp文件各一个,用类的名字命名;所有这些
类,我们还需要一个driver.cpp,调用它们。以上,明确了1.我们的代码需要生成,2.我们需要生成哪些东西,3.这篇博客的
主体,这些生成的代码都应该是什么样子的。8. 附录,那些文件 的代码以下是我们明天要生成的代码。它们现在也可以编译,这样:g++ -I. *.cpp -o go执行的时候:./go此外,从下面的driver.cpp中你可以看出,其实,我已经有了不止mario一个类。
它们都能通过我们接下来要介绍的杨氏语言编译器生成出来。你猜到了,下面的代码,确实就是生成出来的。8.1 mario.h1 #ifndef _MARIO_H_
2 #define _MARIO_H_
3
4 #include <iostream>
5
6 class mario
7 {
8 private:
9 int data;
10
11 public:
12 mario* pipe_a(int par);
13 mario* pipe_b();
14 mario* pipe_c();
15 mario();
16 ~mario();
17 };
18
19
20
使用Antlr+Stringtemplate生成method chaining,一个不太简单的案例(1)
使用Antlr+Stringtemplate生成method chaining,一个不太简单的案例(1)
引言
听着douban电台,很多新的旧的听说过和完全不认识的歌者在网络的另一端歌唱。我也不知识他们和她们都在想些什么,为了哪些感动的事情而快乐忧伤,我知道的是,这些歌声是某种数据流,彩色的发光的,在网络中穿行,最后然后驱动线圈振荡,驱动膜片振荡,驱动我的耳蜗。
这些背后,是信息论和电子学,以及无数理论支撑的结果。因为这些,世界才可能令这样美丽。
听说,BBC声称,编程技术正成为拉丁文学一样的东西。我们正尽享生活,就越来越不屑于了解世界背后的原理。令人不禁想起古罗马一片欢乐声里那些正催吐,以便吃下更多美食的达官。他们正忘记如何战斗,也正失去古希腊探索世界本源的精神。
读罗马史,我看到了散落一地的破碎镜片,很多碎片闪着耀眼的光,也同样是在这些碎片中,我看到我们自己的影像。
----
计算机由语言操纵运行,语言表达了人类的思想。人类的,或更精准的更形式化的语言要转化为机器唯一能了解的代码,然后,机器方能听命于人。
这种翻译人类可识读语言(C/C++,java,python,perl...html)为机器语言的工具,名为编译器。
Antrl是一款编译器生成工具,Stringtemplate是同一作者开发的模板工具,供编译器在解析输入文件后,填充模板中保留的占位符为另一些东西。此作者是一位大学教授,网页上的照片把两只手张开放在脑袋旁边,不知是在戏仿兔子还是蝙蝠,孩童一样微笑着。
他的硕士研究生,做了antlr+stringtemplate的PPT报告,用了童稚的字体,还有他模仿导师动作的个人照片。
导师模仿兔子,我们模仿导师。人类的动作具有丰富的内容,据说甚至承载了超出语言的信息量。不过,动作和表情传达的信息又是模糊的,对于计算机而言,模糊的指令甚至不如没有指令。
在讨论计算机精确的指令系统以前,我想先介绍一些别的。然后到了明天的博客,非计算机专业的同学,就基本可以无视了。
刚刚,我在音乐里睡着了,醒来的时候手里捧着罗素的《自由之路》还没有掉到地上。也许,我只不过迷糊过去了不到一分钟。如果我注意了刚刚在听什么歌,也许我可以判断时间。但是,我唯一知道的是,灯光仍亮着,屏幕仍黑着,长夜仍刚刚开始,我正读的,仍然是那一页
政治与自由。
一般我们认为,罗素是数学家,逻辑学家。他同时还有另一个身份,他也是一位哲学家。他以哲学著作获得诺贝尔文学奖,因为他的文字中对于人类的关怀。
在读《自由之路》的过程中,我不断地发现许多书页中夹着的暗红色叶子,并为此而感动。这当时许多年前某位读者放进去的,某个秋天,她读了其中的很多页,把那个秋天的纪念放在这里。后来的很多位读者,都看到并保留了这份心意。
我执意认为,这位读者当是一位女士。因为男士少有此种闲情。而她之所以令我感动,并非出于性别,而是智慧。读了这些书页的,能读懂这些书页的,应该具有与性别无关的智慧。我见过许多美女抱怨"他喜欢我一定只是因为我的容貌",就像男子抱怨"她喜欢我绝然是为了我的钱"。他们和她们都没有想过一个问题,那是否因为你的智慧并非明显地超出你的容貌或者金钱。
当你的智慧超出性别,那么人们自然尊重你的智慧,与性别无涉。当你注视罗素在很多年前,把他的智慧倾吐在这些纸上的时候,你见到的是一位长者,还是一位老帅哥?
尤其是当岁月洗去他的容貌和声音以后。当你与他的生活毫无交集,因此绝无情感置于你与他之间。
你所看到的,是一个人,他在探索人类所未知的领域。
那些领域,哪怕仅是我们自己所未知的,而别人尽已了解,也仍然是非常有意思的事。
而有些人,不是这样做的。他们并不想了解这个世界,只是想消费它。
听到一个笑话,就是牛顿说我不是牛顿,我站在一平方米上,所以我是帕斯卡那个。我看到有人说:我是文科生,看不懂,猜那是公式,所以我笑了。
所以我叹气了。有些人,并不想了解这个世界,只是想消费它。牛顿每平方米不是大学物理中的公式,而是高中知识。也就是说,能说"我是文科生"的人,既然已分文理,他一定是学过这个公式的。
他只是不愿意去看去想,这个世界,只是他发表意见的垃圾筒,他根本不屑于明白。我确实十分不明白,对于一个你不了解的领域,这整个世界,你怎么敢于发表一丁点意见。
对于我们未知的领域,保持探索,这是后面这个例子的引子,也是antlr+stringtemplate这系列博客的引子。
-----
这个例子,是探索仅我未知的世界,这个领域的人,早就知道了。
前几天以蜡烛做为质料,做了几个小东西,有小兔子,有小娃娃,还有一头小驴子。后来想做松塔,失败了。
其中一种方法是这样的。
第一步,把橡皮泥在橡皮泥的模具(材质是塑料)上压紧;
第二步,小心揭下橡皮泥,不要整变形了;
第三步,把热熔的蜡烛烧在橡皮泥里;
第四步,等蜡烛冷却凝固,把橡皮泥扣下来。
似乎是这样的,这些步骤在某个学科里都是有专门名字的,这些东西也是有专门名字的。
我仅约略知道,在这里,第一步中的橡皮泥模具,塑料的那个,称为母模(还有一种说法,公模母模,按这种说法,这应该是公模);第二步里以橡皮泥为材质的那个,称为阴模,或者简称模具;第三步和第四步里蜡烛的那个东西,叫做什么呢,我们称它为产品吧。
以上的这些步骤看似简单,其实里面诸多细节,任何一个细节的卡死,可能都会令你全盘失败。
有的同学会问,都成功80%了,那也叫失败么。谁说的来着,失败只有一种,就是半途而废。后面种种理想,只要没有实现的,也不过是你大脑里的一些神经电脉冲而已,能有几毫瓦呢。
凡是没有做出产品的,就是完全失败的。这也是为什么某些表现为领导者的干部令人厌恶的原因,他们完全没有实施的能力,只有观点。
在操作以前,有些细节,由于学科训练,我能够想到,有些,则完全没有预料到。
我想到的,书里也提到,蜡烛加热是件危险的事,要始终看着火温。我们都知道那是易燃品。我想到的方法是水浴,即隔水加热,这能保证在加热中蜡烛只会融化,不会燃烧。
水浴,这个方法很令我得意。我还跟包师弟吹嘘来着,同时提到数据,石蜡的融点是47-64度。包师弟说,水什么浴什么热,我做个电子的恒温器。
这就是能力差别。因为他还将避免另一个我在实验时发现的问题,烧铸的时候,蜡的温度非常重要。如果温度过高,会把橡皮泥中的水析出来,留在橡皮泥和蜡液之间,这非常影响对模具细节的表现。如果温度过低,蜡液将开始成为半流体,容易断裂。
我猜到,有些同学正对这些细节不屑。那是因为你只有观点,既不打算了解这个世界,也不解它,因此并无亲自实施的习惯。
这些细节足以使你的作品变成垃圾。
就像你的那些错别字,那些矮油的感叹把你从一个严肃讨论问题的公民转变为一个戏谑或无知的看客。因为你既不尊重你讨论的对方,也不尊重你讨论的问题。这正如打CS的时候作弊,你当然有这样的自由,但是没有人乐意陪你玩,又或者陪你玩的人是和你一样或更不严肃的人,你们一起把这个游戏变成甚至不如一个的游戏。(这里,感谢子龙和兔子的教诲,打CS作弊是不对的。)
同理,antlr+stringtemplate中的细节,也都是非常重要的。仅知道它们干什么的工具,并不能帮助你有能力使用这些工具--仅达到指手划脚的能力吧。
为什么我要提铸模这个例子呢?
因为从明天的博客开始,我会使用母模、阴模、产品这样的比喻。
我将谈到:antlr, stringtemplage的基本原理,我将用它们从下面这样的杨氏语言中生成c++源代码,包括头文件,cpp文件,调用它们的cpp文件,能编译并执行的。
杨氏语言的一个例子:
mario:pipe_a 123 | pipe_b | pipe_c
peach:stage_1 123 | stage_2
bowser:lose_1 123 | lose_2 | lose_3 | lose_4 234
没错,这就是超级马利的一个粗糙模仿。马利同学依次穿过了pipe_a,pipe_b,pipe_c,并且在pipe_a那个场景里得到了123这么个道具。
最后生成的cpp代码大致像这样:
o_mario->pipe_a(123)->pipe_b()->pipe_c();
o_peach->stage_1(123)->stage_2();
o_bowser->lose_1(123)->lose_2()->lose_3()->lose_4(234);
生成的代码中,还包括o_mario等这些对象的类的声明和实现。
另外,下面这种调用方法,就是fluent interface的实现手段之一:methodchaining。是不是看着挺人性化的?
o_mario->pipe_a(123)->pipe_b()->pipe_c();
明天开始,我们整个模具生成它们。明天,我们先看母模啥样。
pics
我想买个键盘:用户需求可以多么刁钻
我想买个键盘:用户需求可以多么刁钻套用那句著名的"我想要的很简单",今天我想要的也很简单,我想买个键盘。为了这个简单的愿望,不仅昨天在网上查了半个晚上,今天又跑了半天。这证明,所有的用户需求都是不简单的,包括某些看起来非常简单的。只要这个要求是特异的,那么就不简单;如果这个要求不是特异的,它根本就不会存在。如果我要求键盘上面有ABCD几个字母,那么你就会哈哈大笑,这个要求容易满足。如果我要求这个键盘只有数字,要非常的小,你会问我"是不是银行里输密码的那种布局"。如果我明白什么是布局的话,那么你我就可以很好的沟通。如果我想要的是无线蓝牙或者有线,那么你可能推荐我罗技。这些,都不是我想要的。1.我想要的键盘应该只有"基本区":用户的基本需求,不同于平常的键盘从左到右可以划分成三个部分,最右边区域是数字小键盘,中间的区域是翻页和光标移动键,最左边的区域就是我说的"基本区"。我要这样的一个键盘,它从基本区的右侧切了一刀,右边的都不要。昨天晚上,我在网上找,"迷你",找到的是一巴掌宽的那种。我想要不是这种,而是尺寸正常,只是切除了右半边的。2. 我不喜欢苹果:用户需求变更在百脑汇,老板们给我找到了不少符合我上述需求的。但是我突然发现,它们有个共同特征,像苹果的键盘。苹果引领了新时尚,但我打算不跟随啊。苹果键盘的键帽,表面有个不同以往的特征。咱们传统的键盘的中间是凹下去的,而苹果的键帽上面是平坦的。那个洼洼兜对我非常重要,它能让我在全黑的环境下也能确保每个手指都在正确的键位上。F和J上的小突起,仅能帮助我定位这两个手指的位置,但是我是否按到了键盘的边缘以外,是不是一直都打在键的正中心上这个洼兜。平坦的键顶,让我恶意猜测苹果的大多数用户都是偶尔才使用键盘的,或者键盘只是玩具,而不是工具。可能不止苹果,所用的计算机用户都步入娱乐的时代了。无线键盘很难找到单独卖的,而鼠标和键鼠套装却可以,也是一个旁证,大家不再那么需要键盘了,而是更依赖鼠标。鼠标可以用于选择,就像答ABCD的选择题,鼠标也能用来表达思想么?就像转载能用来传达态度,转载也能用来表达思想么?我需要工作,需要即使在黑暗中也能准确定位按键,所以,我不能用苹果风格的新潮键盘。有人可能会建议开灯低头看一眼。恩,有一种技术叫做盲打,是程序员的基本功。
3. 右边的CTRL:用户说,看到了才知道,这不是我想要的终于找到了符合上述要求的。此时,如果是我们在做用户需求,可能已经报怨过了,"你不喜欢苹果的,为什么不早说,我白给你拿来。"此时,你还会再次抱怨。因为符合上述要求的,仍然不符合我的要求。因为用户说"只有当我看到了你的作品,我才知道,这不是我要的。"而用户想要什么,你永远也不能提前预知,因为连他自己也还不知道呐。我就是在看到了符合上述所有要求的键盘,并且按了几下,才发现,还是不行。手感啊,键的行程啊,按下去半天不弹起来啊,没有后背上方的小支架啊,这些也就算了。20元+的键盘,你还能有什么更多的要求呢。可是,怎么可以没有右边的CTRL键。我之所以想要一款这样小的键盘,是因为我用EMACS编程。所以,1.我不需要光标移动键和翻页键,2.我有时会用鼠标,为了编程的时候上网查资料。这时右侧的数字键和编辑键区域就是累赘,那正是我的右手腕要通过的区域,我的右手腕从那里伸向鼠标。你可能会建议,把键盘向左移一下,不就行了么?如果键盘向左移动10厘米,确实为右手腕空出了地方,但是当我要用键盘输入的时候呢?我的手腕不得不为了键盘在左侧而扭曲。而编程,毕竟是键入比上网的时间要多。如果手腕再向右呢?你可以试试右臂张开的角度,并保持一段时间,而此时你的键盘"基本区"在你的正前方。顺便说一句,我不需要数字区,是因为1.我很少连续输入数字,2.我能半盲打所有的数字和它们的上档键。这些跟右CTRL有什么关系呢。用户MOJI完上面这些,你可能会问。用户会接着 MOJI下去,他认为重要的事情,并传达一下情绪。很多人用EMACS的时候都会遇到一个问题—恩,很多问题,这是其中的一个。许多人用EMACS时间长了,会左小手指疼,因此有些人还会把CAPSLOCK和左CTRL交换。我没有这个问题,一个原因是我不仅使用左CTRL,也会使用右CTRL。Ctrl-C,就是用右CTRL。而且,这是标准做法。问题来了,符合上述所有要求的键盘,居然个个的右CTRL都缺失了。也不知道是大家伙山寨哪位大爷最初设计的结果。应该是CTRL的地方,换成了INS和DEL键。DEL还算有用,我一辈子能按INS键几次啊。4. 后来我终于找到了基本符合要求的:拒绝用户部分需求其中一款是微软的,300元+。小众么,就会是这么个结果。非量产,小资情怀,纯手工,它们都具有共同的特点,那就是贵。后来发现,它是苹果风格的,之前只看大家评论的优点,忽略了。另一款不那么贵,不到100元。右CTRL非常窄小。这适用用户需求获取的另一个原则:拒绝不那么重要的用户需求。我准备映射一下,把右边那几个键都改成CTRL,这样就不容易按错了。用户是上帝,前提是用户付得起钱。或者说,用户打算承担由这些变更或需求引起的时间、费用、人力上的开销。用户喜欢花样百出,但是只要你坦诚地告诉他,花样都可以满足,不过是要收费的,用户往往就突然变得不喜欢这些花样了。其实我们应该喜欢这些花样,这是我们存在的原因和用户付我们钱的原因。但是,往往天不遂人愿,工期或者用户的荷包刚好不敷。这个世界上,就没有简单的需求。无论多小的需求,都可以极尽刁钻。不仅缺失的是问题,比如右CTRL,有时连多出来免费提供的也是问题,比如编辑区和数字键盘区。还有的用户,像孩子一样,只会哇哇大叫,表达的是"我不舒服我不爽"。但是他需要的是什么,连他自己也不知道。这又让人有什么法子呢。我们,就真的知道自己想要的是什么吗?如果你真的知道,为什么没有去做,没有尽力去做。你真的想要的是什么?