Category: 未分类
穷人如何使用测试驱动开发进行重构
穷人如何使用测试驱动开发进行重构重构这个词现在已经被用烂了。我们经常听经理啥的说,咱们应该把系统重构一
下。当他用重构这个词的时候,想表达两个意思。一,把这个系统重写一遍,大
手术,二,我很fation.重构具有更严格的意义,就是需要在重写的同时,保证系统原有的功能。如果对
系统原有的功能不变做出保证呢?我听过很多人保证:我的U盘绝对没有病毒。你用什么保证。我见过用脑袋保证
某事的,最后,他的脑袋还留在他自己的脖子上。我并非对他的脑袋搬移有什么
兴趣,而是想说,他的脑袋确实很值钱,但是于我却毫无用处。我们用什么保证?胡适先生说:证据。测试驱动开发能给出证据。测试用例如果在重构前和重构后都通过,就说明重构至少在功能不变上是成功的。重构有不少好工具了,可是对于我等穷人,没有大公司啥的作为支撑,有时机器
连eclipse跑不起来都不顺畅。没有利器,如果重构?测试驱动开发也有不少好工具,可是如果你用的既不是C++也不是Java,甚至
连.net也不是,cppunit,junit这些工具又如何助力呢?刘慈欣先生在《全频道阻塞干扰》的最后,让被逼到毫无退路的美国军官说:我
们的祖先也不是最开始就有坦克大炮导弹核武器的,士兵们,上刺刀。我们总可以在更原始的工具中找到新技术的精神,因为牛人们从前就是以此这些
丑陋陈旧的工具创造了这些新的技术。昨天还是前天,二猫像个小大人一样一直坐桌前陪我吃饭,跟我聊。谈到她喜欢
吃这个吃那个。这几乎就是目前她生活的全部,所以有些事情是她想不到的。我说:爸爸小的时候没吃过香蕉。她问:为什么不吃?你不喜欢吃么。我说:因为买不起。她问:香蕉很贵么?我说:不贵,是我没有钱。我曾经教育才外教Dave同学。是的,教育,爱国主义教育。他看到我读《鸿:中
国的三个女儿》,问我:允许你读这种书么?我说:啥?为什么不能。他说:这书不是中国出版的。这书确实不是咱们出版的,是阿于同学从美国给我带回来的。这面这段对话可以看出,他对于陌生的国度有怎么的不了解。所以,在后来,我
对他进行了如下教育:我问:你是不是以为俺们政府真的就挺虐待她一家的,她家还给俺们做出那么多
贡献和牺牲。他说,是啊。我说:那我来给你讲一些事。作者,五岁的时候,因为不喜欢上幼儿园,把牛奶倒在桌子上,挨了批评,很委
屈。Dave说:我记得这个情节。淘气的女孩。我以为,不打死她个败家的就已经是手下留情了。中国很多孩子至今都不能每天
喝上牛奶,我在大学以前,就很少喝到牛奶。他很难相信。我再问:你记不记得,她妈妈去医院(或者临时监狱?)看她爸,因为不能坐单
位的车(她爸单位的),而只能骑着自行车。你认为这很艰苦?他说:是。我告诉他:那是195X年,整个中国,就没有几个人有自行车。我想,不亚于现在
的宝马吧。直到197X年,20年后,自行车仍然是结婚时必备的四大件之一。可
见,它多么昂贵。同样的数据,同样的事实,我们可能得到完全不同的结论。但是,即使我们买不起香蕉也仍然活了下来。重构,测试驱动开发,也是一样。
尤其是在最艰苦的时候,比如你所用的语言没有测试驱动工具,就更能体现出穷
人方法的必要性。1. 我们假设所有的输出都是向控制台的。至于gui程序,你可以先写一个console版本,让未来要写的gui与console版本使
用相同的业务逻辑库,凡是与计算机的机制无关,而与用户特定需求的,都放这
里。gui消失触发以后,别的啥也不干,调用业务逻辑库中的某个函数。2. 先跑一把程序,整个输出,重定向到一个文本文件里。这时,输出的文本文件十有八九不是你想要的。因为特别简单的程序,会轻量级
到甚至无须测试驱动开发。3. 手动修改这个文本文件,改成你最后需要输出的样子,重命名为
expected.txt。这就是以后我们用以对比的素材了。4. 跑你的程序,改它,再跑,再改。估计好多人都要先改语法错误,让编译通
过。始终把输出由控制台重定向到 output.txt。这样做:假如你有输入文件的话,那么,假定它命名为 input.txt。program.exe < input.txt > output.txt5. 当你需要测试驱动的时候,执行这样一条指令:diff output expected.txt如果啥结果也没输出,成功了。我第一次见到比较成功的时候没有输出,很惊度。
李记者教导我说,diff么,就是找不同,没有不同,它就应该不吱声。如果有任何输出,你的程序仍然没有符合要求,继续改,继续跑。6. 以上,是测试驱动开发。测试驱动开发,是重构的前提。没有测试驱动,一
次次用眼睛在程序输出里找你想要发现的错误或者期待的字符,你就是在构建性
能可怜而其实非常宝贵的人肉计算机。如果不同太多,你又想找特定的字符,请使用 grep。7. 以上的 diff 和 grep 是标准unix系统的小工具,所有的Linux版本里都有。
如果你使用windows,那么,可以下载 mingw & msys,或者 gnuwin
[http://gnuwin32.sourceforge.net/]。8. 关于重构,补充一点。想各种办法,始终保持约20分钟内,代码可以编译和运行和测试通过。尽可能不
要冒险长时间不编译或不运行或者测试不通过。你可能会期待在一个大~~~的不间
断的重构之后,代码一下子跑起来了,而且正常了。这就像打算沿着海岸游泳到达某个地方,我们可能遇到一个海湾,抄近路似乎一
下子就过去了。你可以想像一下,从丹东到青岛,一种方案是沿着渤海湾一次半
公里,另一种方案是取直线。取直线,真的是极富有吸引力。毕竟,我们编程大
半是为了快感,而不是为了资本家。可是,如果你沉了呢,后果很严重。这也是
为什么需要测试驱动开发的原因。小步的迭代,每步都要确保能活着再次接触海岸。我妈说了:宁走十步远,不走
一步险。我曾经4个小时左右,不编译,不执行,不测试,这个期间程序就跟大手术尚未缝
合,根本不能跑。一下子跑起来,是的,非常兴奋。可是,也有几个小时以后完
成失败,甚至回滚到几分钟前都没有意义,只能一下回滚到几小时以前。那种挫
败感也令人印象深刻。这些经历,我知道你也有,就像牛仔的伤疤,如果没死是
大可以拿出来显摆的。不过,还是每天写一点,每小时写一点,20分钟写一点。以微小的可控的单位前
进。像竹子那样,每走一步,就要做个小节。未来无限美好,此刻却虚无飘渺。如果有经理老板导师男朋友女朋友对你这样
说,你就给他讲讲测试驱动开发和重构的道理。----完成所有今天能完成的任务,喝一杯咖啡,然后倒头大睡。人生当如此,哪怕就
此长眠,也了无遗憾。
pics. gone with the wind
一见钟情 vs. 众里寻他
一见钟情 vs. 众里寻他1.搜索与遍历一见钟情 与 众里寻他千百度,是爱情的两种境界,这在文本编辑和软件工程中
也经常遇到。我们最经常进行的文本编辑是编程序。请考虑一下这个情境,你要跳到文本的某
个位置。相信有不少人的右手伸向了鼠标,是打算去用滚轮了吧。但是,这并不是最快的
方法,而且是一种打算你思路的方法。什么样的情况最可能发生一见钟情?当你不知道自己想要的是什么样的人。那
么,你就只好一个一个一个一个看下去,直到遇到某一个,你感叹"啊,原来上
天为我准备好的那个人就在这里!"心理学告诉我们,在这之前,你的心中早就有一个对象的原型了。但是,你一直
可能都不知道。你看那一个一个一个一个的时候,每个都要判断,是这样么,是
那样么。如果我们换成众里寻他模式,情况会完全不同。这时,你事先就知道自己想要的
是什么。我们俗一些,比如:年薪百年,年少俊美,恩,还有很简单的要求,大
落地窗,大大的浴缸和大大的床。对了,还有满床的阳光。那么,就好办了。你根本不用一个一个...地看下去。因为全世界绝大多数人都
不符合你的要求。要求越明确就越容易实现,或者被否定。有人可能会说,这本身就是一个学习的过程。爱情我就不懂了,文本编辑的时候
跳转到你想去的地方,那不是一个渐近的学习的过程,而是:要么,你知道自己要跳到哪里;要么,你不知道自己要跳到哪里。如果你一行一行地看下去,停下来挠挠,上上人人网,听会歌,又很心烦,又一
行一行看回去。我打赌一块钱,你不知道自己想要到哪里去。请对比女性和男性去商场买东西。女性可以不断地比较,了解,徘徊,而男性直
达目标,买到或买不到,然后就走了。为什么?因为我们的祖先里,女性是采摘
者,她们可以站那儿比较这个果子和那个果子,果子是不会跑掉的也不会烂掉的。
而男性是猎人,就这么一只兔子,打还是不打,一犹豫,肥不肥,毛色如何,白
不白,可爱么。这些都是好问题,不过,你的兔子已经跑了。更可怕的是,当你在森林中遇到一头黑熊。然后,你开始犹豫。我们应该做的是:断然按下 ctrl-s 输入你想要去的地方,然后回车。接下来继续你的工作。一行一行地看下去,你也会找到想要的地方,但是这中间的过程,会打断你的思
路。这就是目录、书签、回城卷轴存在的目的。当你去找某个朋友的时候,会挨个屋子推开门,然后施施然进去一个个对比么。
先知道你想要的,然后行动。遍历一行行,就是在等待自己终于知道想要的是什么。这个过程也是很美好的,
可叹人生缺少时间去经历。或者,别人缺少时间。2. 搜索 vs. 选择常看到有人这样看网页,一下下往下翻,只右手抓着鼠标,眼睛盯着某一处,别
的地方都不与计算机接触。这是被动的信息获取方式,就像发呆的同学看着老师的嘴一张一合。你同意某个
观点么,你不同意某个观点么,你事先预想了作者可能的观点么,你如何表达赞
同或者反对。还是,你毫无意见,只是想经历一下这个世界。看到不少同学在使用搜索引擎时遇到困难。为什么他总是找不到想要的东西呢?
因为他没有预设当他要表达这个信息的时候,会使用什么样的措辞。更深层次,
因为他不表达。搜索引擎需要你大声地喊出你要什么,然后他帮你找到。在网络上还有另一种东西,与搜索引擎不同的,类似黄页,叫open directory,
比如[http://dmoz.org/]。它像个殷勤的小跟班,老板,你要这个么,老板,你
要那个么。它与搜索引擎最大的不同是:它不需要你*主动*地表达。你只要对某个选项点头
就可以了。想到ABCD单选题了么。当你拥有选择的自由的时候,你失去了所有其他的可能。
只有随心所欲的表达,才是真正的自由。如果心里没有,那么连枷锁都不
需要。主动搜索,而不是选择各种别人提供的可能。武侠小说里的高手们,往往能在对手有弱点的地方,等着对方撞过来。那是因为
他们预判,知道对方要做什么。对未来的预测,也是主动搜索的一种。更高级
的,不是等对方如何,而是引导他去做。3. 规模在信息量少的时候,你当然可以慢慢地阅读。我想这是大多一只手看网页的人这
么做的原因--他的生命有足够的时间。但是,有时不行。比如系统管理员查日志的时候。日志有成万上百万行,你绝无
可能有足够的时间一行行看下去,慢慢形成对故障现象的认识,一行行去看某行
是不是记录了故障发生的原因。所以,你必须事先知道故障看起来应该是什么样子。然后,不是让那些行在你眼
前飞过,抓住符合特征的那行,而是使用grep,用匹配字符串或者正则表达式匹
配来搜索故障。使用这些工具,瞬间,通常不到一秒,可以完成。前提是,你要知道你要想的是
什么。并且,你要用语言描述出来。五笔打字比拼音快的原因,就在于,五笔是不必选择的。时间就消耗在你在一大
堆选项里跋涉。命令行比菜单快的原因也在于此。旋钮的微波炉比按键电脑的容
易使用的原因,也在于此。因为不必交互。如果你找不到你想要的,而他事实上是存在的,十有八九,是你并不知道你想要
的是什么。3. 现实生活而现实是残酷的。我们经常见到点菜的时候,你我就这么做过,我们对付账的人说:随便。我们经常见到领导不明确说出自己想要的,而是等下属提出或实现出几种,然后
点头一种,其余的否定。我们经常见到清宫剧里的皇儿上儿,是这么个发音吧,他一句话也不说,等奴才
们实现各种方案,然后嘉许一种,其余的斥责。我们可以看到,这些主子并不表达自己想要的还不是最糟糕的部分,最糟糕的部
分是他还会对你的实现做出评价。所以,那些能指别人需要却不能或无能说出来的人,都是牛人。比如心理医生,他说:你看,你就是这么这么这么想的,所以这事也没啥。然后
你点头,对啊,我早就该知道我是这么想的。你之外的另一个人,比你更了解你想要什么。但是,说"对对,这就是我想要的那个人",在现代社会,往往是病人,而不是君
主。或者既是病人,也是君主。你是不是点头了?对对,这就是我想要的。我们习惯于在交互中表达。我们说了一小段,然后等别人的肯定或否定,或复
述,然后才敢于或舍得继续下去。而在真正有价值的交流中,这样的沟通太低效
了。当你写作论文,文献,手册的时候,如果你的表达不够清晰严谨,读者要做的比
喝令你闭嘴还简单,他会直接关闭文档转身走开。他绝不会巴巴地问"您老要说
的是不是啥啥啊",然后你说,"着啊,小某子,进步了啊。"这绝不会发生。想要求别人理解领导的心思,需要一个前提,那就是你得是领导。而写论文、文
献、手册的人,往往都是被领导的。哲学家们写作时也是一样。他们思考世界我们啥的都是怎么回事。可悲的是,每
个时代,或者几个时代,才出一个哲学家。他们都非常孤独。我读罗素<西方哲
学史>的时候,每每感叹,谁谁太孤独了,等到一个能读他的人出生的时候,他
都死了好几百年了。如果这些哲学家们以信赖于交互的方式写作,他们对于世界的思考,一行字也不
会传下来。他们边写作边猜测,我们会如何理解这段话,会有二义性么,会误解
到什么程度。我们读不懂,不是他们写得诲涩,而是因为这东西就是这么难,同时,
我们就是这么面。我们面到不仅不清楚领导想要什么,有时候甚至不清楚自己想要什么。人人网上我见到有同学抱怨机房在课前开得太晚了,只有"到点"的时候才能放同
学们进去。我提醒一点:机房的老师要做准备工作。要做哪些准备工作呢,我不权威,只能猜测。机房的卫生(请回想我们扔的瓜果
皮壳和饮料瓶)、重启机器、维护机器处于正常状态、电力系统保养。如果每次课前没有打扫,同学们可能会抱怨卫生不好。如果打扫,同学们可能会
抱怨机房没有"提前"让大家进入。我们有时候不知道自己想要什么,或者,没有意识到我们想要的是矛盾的。又想
让马儿跑,又不想让马儿吃草。不仅同学,老师们导师们老板们领导们也经常如此。4. 表面表面上,我们只要知道自己想要的,就可以搜索了。但是,很多时候,我们所知
道的,不过是表面上的需求。用户需求到分析,这个过程可以use-case driven,让用户讲故事啥的,了解他
们想要什么。他们一定会说真话么?我们自己,说的就是真话么?有人追求传统,有人追求流行。表面上看差异很大,无外乎追求 认同。追求传统
的人说:我是优秀啥啥,才不屑于啥啥。追求流行的人也这样说。但是,在否定自己不是什么人的同时,传统与流行,都不过是*别人的*,老了的
死了的那群人认同的观点,或者当前大众认同的观点。连追求小众的人,都是希望更好地获得小集团的认同。为了小集团,甚至不惜违
背更大范围和更长历史的认同。我们自己,说的就是真话么?我们觉得大人物们渺视我们是他们愚蠢,觉得我们渺视更小的人物,那也是因为
他们愚蠢,但是我们没有质问人物应该有大小么?我们追示隐身查看好友,追求
星星月亮图标的的差异。我们想开宝马车,想坐红旗。表面上,我们追求的是与
众不同,但是,我们追求的是与某一部分"众"不同,期待的是对我们的高等级的
认同,对我们所获得的特权的认同。我们批评学生,表面上是为了他们好,而有些时候,不过是为了维护自己的威权。
更有甚者,我们还可以说:这是为了让他们到社会上的时候不吃亏。我们自己,说的就是真话么?5. 规范以上,搜索时,我们希望自由地随心所欲的表达,但是,这真的那么容易实现
么?为什么会有菜单的存在。是为了规范和限制一部分人的行为。写代码的时候,黄同学告诉我:appfuse会按java规范,把所有下划线删除,把下
划线后的字母转成大写。这可以避免程序员犯非常愚蠢的错误,强制使用骆驼符
号法,同时它拒绝了程序员的自由选择。因为它认为你是愚蠢的,所以,你没有能力自由表达,再所以,你没有资格拥有
自由表达的权利。我们捂住孩子的眼睛,我们给他们一块红布。告诉他们,这个世界是什么样的,
而不允许他们伸出手去触碰哪怕一点点细菌。请不要联想太过丰富,至少在工程上,菜单有存在的必要。不是受用户的智商
所限,而是他们时间太紧,没有功夫接受培训。6. 领导当领导划出道道来,说这就是解决问题的全部可能方案,你只需选择的时候,那
就是你失去创新的时候。那是领导的创新。在工程上,我们按受上级指派的任务,按规定的方案实施。那是因为技术的差异
和责任承担范围的不同,我们在自己的领域内仍然可以创新,不是因为上级的等
级更高,也不是因为上级拥有更多更好的特权。当GFW封住因特网,告诉我们只要看什么就能富国强民创新科技的时候,我就想
起了以上这些。
pics
罗马十二铜表法的进步与软件工程需求与原型实验
罗马十二铜表法的进步与软件工程需求与原型实验"十二铜表法就其阶级实质说,是一部分严格维护私有财产,维护贵族利益的法典。
然后,十二铜表法的颁为罗马法的发展奠定了基石,是整个罗马历史的发展过程
中的一座里程碑。尽管法律条文极力地对贵族的特权地位加以维护,例如,在第
十一表的内容中,强调了"平民与贵族不得通婚"。可是,制定法律并将其以文字
形式公布于众,本身就是法律史上的一个巨大进步。从此,国家对法律行为的有
效、无效、赔偿、处罚等都有了明确的规定,在一定程度上限制了贵族官吏的专
横。"罗马:从共和走向帝制
宫秀华 著
高等教育出版社
2006年7月第2版这个故事告诉我们,在软件工程中,清晰的需求,哪怕是错误的,也比模糊的需
求要好。甚至可以说,模糊的需求比没有需求更坏。陈述观点的时候也是一样,清晰的观点,即使是错误的,也可以拿出来作为靶子
进行充分的讨论。鲁迅先生说:水有的浅而清,有的深而浊,有的浅而浊,有的深而清.世间的人,大凡
可归此四种.宁可要浅而清的人,也不要深而浊的人.所以,我们听完别人的观点,可以复述一遍:我理解的是否你的原意?当我们读完教材或者文献,不妨做个实验,验证一下我们对于理论的理解是否正
确。这就是工程原型。如果原型是错误的,我们的代价相对较小,如果没有原
型,等到工程正式实施的时候,代价则可能是惨重的。当我们操作真实世界的时候,由于要花费水泥沙子木料,所以我们相对谨慎。其
实,当我们操作虚拟世界的时候也是一样,因为我们正在花费更宝贵的东西,你
的时间。尽早发现错误,则损失比更晚发现要少,因为如果错误保持下去,我们还将在此
基础上做很多工作,这些工作都将废弃。尽早发现错误的一个方法就是,尽可能清晰地描述。这就又回到了老问题上:你真正想要的,是什么?
pics
pics
.
源代码:使用Antlr+Stringtemplate生成method chaining,一个不太简单的案例
源代码:使用Antlr+Stringtemplate生成method chaining,一个不太简单的案例在[http://antlr.org/share/1323101639207/antlr.zip]里面有所有源代码和正文。
使用Antlr+Stringtemplate生成method chaining,一个不太简单的案例(6)
使用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。
祝你开垦顺利,有收获。