20111231

当一名战士就是一支军队,那些不需要软件工程的时候

当一名战士就是一支军队,那些不需要软件工程的时候

* 最初的代码

1994年,当我开始对编程感兴趣的时候,还没有软件蓝领这一说法,但是我已经
有了后来软件蓝领流行起来以后的困惑。

我第一次做的比较大的程序,是用GW-BASIC写的,没有IDE界面,需要按行号插
入,黑底绿字的显示器,单个软驱倒腾用两张盘。 (感谢我们的导员刘春光老师
每天中午借我用他的计算机) 要编的程序是自己想出来做着玩的,一个DOS界面下
CGA显示模式,菜单方式的……班费管理程序。如同齐同学的那个定票系统,这个软
件并没有实际应用,不过,它对我来说,比此后所有写的程序都更难。

代码后来参加一个比赛的时候,打印了唯一的一份纸质版,打印纸抻开比我举起
手还要高。我当时遇到了程序设计中的核心问题--大量的代码,复杂的逻辑。

我当时使用了GW-BASIC提供的一个非BASIC的功能 gosub,类似于函数调用,它帮
助我逃过了程序彻底混乱的厄运。后来当我学到模块化思想的时候,如遇故人。
我毫不费力地就接受了这个观念,因为痛过,所以印象深刻。

后来经常见到有初学的同学函数写得超出两三屏,还很得意自己逻辑控制能力。
我就在心里撇嘴,你那是还没受够罪。

大量的代码,复杂的逻辑。软件工程给了我们某个答案,就是软件蓝领,它声称
大量的人工、短期培训、重复地简单劳动,能够解决--以工程的方法--大量代码
和复杂逻辑的问题。

是的,我们这么干过,好几千看前就这样做。埃及盖金字塔,是没有起重机的,
而是靠几千几万人力完成的;中国的古长城 (不是当代的) ,也没有等待现代电
子计算机和通信技术的发展,而是靠万喜良们的双手堆砌出来的。

那个时候,他们一定期待一种东西,可以用燃油作为动作,稳妥精确地运输沉重
的材料。

但是他们没有。因为是时代是父亲是民族选择我们,而不是反过来,所以很多时
候很多事情都不能一蹴而就。

有的时候,智力或自然的法则也参与限制。

* 他们说,没有解析解

在数学当中,有一种解题的方法得出的结论称为解析解。我们解一个方程,得到
结果,如果我们所做的常见运算只需要 有限次,那么,这个结果就称为解析解。

这是什么意思呢?就是说,你可以通过公式,只需要一个大式子,可能非常大,
但是最终可以计算出结果,直接地。

难道不都是这样么?不幸的是,还有一些方程,伟大的牛人数学家们告诉我们,
有些方程就是不能通过公式求出来。而我们在工业生活中还需要求解。

数学家牛人们还是有办法的。他们创造了另一种方法,用猜测-比较-再猜测,大
致这样的方法,逼近我们寻找的那个数。这些牛人们中的第一位就是著名的牛顿。

但是,我们得到的是那个"数",是整个方程中的一段,而且是粗糙的。精细的完
全一致的解,可能永远也无法求得,我们得到的就是对于当前的应用"足够"精确
的个案。

人类是多么地热爱形而上,热爱一次性解决所有问题啊。可是,数学牛人们说,
有时候,你哭也没有用,就是不行。

在程序设计中也是一样,只有工程方法,有人说,就是蓝领方法,才能解决大量
代码和逻辑复杂的问题。

如果没有燃油,没有热功当量,除了征服更多的奴隶,又有什么方法能够赢得自
己的自由呢?

但是,我们是否已经判定程序设计一定没有解析解,所以只能靠人力逼近?

* 解析解

我和李记者曾经对刘典同学怀有偏见,认为他(没有虽然技)技术极好 ,但是却从
不注重软件中的工程,也不怎么注重合作。

今天,关同学用事实给了我强烈的教育。她用事实告诉我:软件工程为什么有时
可以忽略?因为有的程序员,她一个人可以完成超过100个程序员的。

就像有的战士,一个人就是一支军队。

刘典同学讲过他写数据库的程序用了编译原理生成代码,讲过写手机游戏的时候
用虚拟机。前几天,我刚刚写了3千多的代码生成器,吐出来近6万行代码。这些
给我的印象也都没有今天这样深刻。

程序设计,是一种创造工作,就像写小说。与写小说不同的,你所创造的是一台
机器,它可以做很多事,你甚至可以制造一台机器,它以代替你写作最终需要的
代码。

在所有的计算机本科都开设了相关的课程,叫做编译原理。在一定程度上,这是
一个解析解。

* 关同学

今天我CIAC的导师请大家吃饭,辛苦一年。导师本人想参加,我托包师弟说:不
欢迎他。如果导师出现,今天稍微拘谨的场面,就可能令聚会完全不同。

我们讨论了,我们吃午饭了,我们唱歌了,我们又吃晚饭了。

刚开始吃晚饭没多久,包师弟说:2012的上半年,我们有一些任务要完成,相当
于本年度完成任务的40倍工作量。

他说:这些工作都是相似的。

可是这些相似的工作如果不能抽象出其中相同的部分,就没有一点相似。我们人
类看到的相似,对于构造代码而言,毫无用处。

我看不出来相似。然后我想了几个方案,又都推翻--我在想从哪里抓那么多奴隶
来,又用什么报偿他们,工程本身于他们何益。其实,同学们并非奴隶,必须保
证同学们有足够利益和受益,否则除了我自己,一个人也派不出来。

我说:包师弟啊,你能不能别在吃饭的时候说这个,我都吃不下去了。

我真的吃不下去了。焦虑。而且,从这以后,我真的几乎没吃啥。

奇迹时刻。

关同学说:老师其实我想了,这些方案都是类似的。

我说:啊?

她说:所有的界面都可以……根据配置文件,new 出 一个 label来……

是的,不熟悉关同学的,对女生能否写好程序有疑问的,请仔细看一下,她,不
是他。

而且,她也不必再解释这个方案,因为软件组可以全体解散,而剩下的工作,只
需她一个人短时间就可以完成。

这就是抽象的力量。

她没有写GUI,而是解析配置文件生成了GUI;她绕过了令我头疼的C#如何表示
GUI--这样就可以生成RC文件,在编译前,我考虑过的方案--而是在运行时,new
出所有的GUI控件来,相当于解释执行的。

* 后来

后来,全体软件组成员加入了硬件组,将承担下位机的代码。很好,我终于不用
再讨厌他们用的IDE了,因为再也没有他们熟悉的VS什么的了。我们都开始进入
单片机或ARM的世界。

后来,关同学对我的赞不绝口指出:这个方案是你告诉我的啊。

我说:啊?

她说:就是大仪网的时候,你告诉我blabla。

我想起来了。不过,这仍不是我的方案,而是她的。一个方案之所以好(像这个,
好到如此突出,以致你一眼就能看到,绝不可能错过,如果你看到了的话),是
因为它被应用在一个恰好合适的领域,恰好解决了一个难题。至于这个方案有多
难有多容易,有多高科技,其实不是多重要。

关同学刚毕业的时候,我们在CIAC讨论一个框架,当时我说:这个倒是可以再抽
象,不过我的方案有点耍赖了。

关同学说:你是不是要用函数指针。

是的。而且我非常欣慰了一下,因为学生优秀。

黄同学当时认为:函数指针,也没啥难的啊。

是的。函数指针一点也不难,能想到用函数指针解决这个问题,是一个高度。

关同学在此刻想到了一个如此好的方案,所以接下来的半年,我们都不必那么焦
虑了。

这就是解析解。

关的方案,不是减轻了劳动,不是像我以工程的方法、各种测试 (关今天还提出
用MATLAB生成测试数据,也很好,后来给齐同学用上了) 来控制代码质量,用框
架规范程序员的行为,这些都不是,关同学直接替代十来个人把40个用例生成了
出来。

代码质量如此一致和优秀,是由图灵保证的。

* 后后记

上午,与一位技术人员和一位经理谈话。

我提到 通用的CMS > 定制的站点 > 使用CMS。

那位技术人员不认可。我说:我刚刚说错了啊,我不是指复杂,而是指困难。

那位技术人员blabla说,这不困难,只要如何如何即可。

我说:其实我们也不必达成一致意见。我的意思不是说我们无法实现,我说的我
会收更多的钱。

争执略去,我同意那位技术人员的下面这个观点 (大致意思,我翻译过的) ,但
是当时没有时间表达:这不是工作量,而是更高的高度。

是的,那不是更复杂,不是更消耗时间,甚至不是更困难。

那就是更值钱。

关同学用事实告诉我:一名战士完全可以是一支军队。没错。

20111221

我早就说过

我早就说过

"我早就说过",经常有牛人说出这一句话,配合气场,一般代表我多么地牛啊,
早就有些先见之明。有时候,还附带对听众的嘲笑,你看,你们居然不识货。

我们常听到:某某观点我们祖宗早已有之,某某技术不过是啥啥技术的翻版……

再来看两个例子。

云服务的。当年,我整了几张大纸,画下了一个方案,命名为"科技改变生活",
内容是把计算和服务、软件都放在主机端,终端都用计算性能非常性的机器,只
负责显示。跟丁老师聊,他认为,这根本不可能。几个月后,云服务的概念正式
提出,当然,由别人,不是我。

我的先见之明值得自豪么?不。因为我只有瞎扯的能力,如果整不出来,我也不
用负任何责任,所以,我也没有投入所有家当去整这个极有前途的东西。

又。ZHUMAO有天提起来,他刚工作,或者还没工作的时候,跟我提到可以在计算
机里面装两个操作系统,一个跑在另外一个里面。我完全不相信,我觉得只有多
启动这一类的吧,怎么能一起跑呢。他说我都装出来了,我也不相信,直到亲眼
见到。说实话,我忘了这事很久了,因为不是我的光荣史的一部分。经他一提,
才想起来。ZHUMAO同学比我在云服务中的表现要好多了,他真的搭了个虚拟机出
来。

我没有这种先见之明很应该惭愧么?也不。因为,即使我能预见到虚拟机巨大的
市场和技术潜力,我也没有能力实现。自己无法实现的事情,远观则可,没有必
要为没生出来的鸡蛋们打碎了而悲伤。

前几天去学习精品课,听到两则八卦,也能说明类似的问题。某学校有个计划:
根据学生的既有表现和举趣,为学生提供下一步的建议,该学生该看什么,都可
以计算出来。

很有雄心的计划,问题是,用什么样的技术手段可以实现呢?对于无法实现,至
少我们无法实现的东西,讨论到细节和投入资金,除了丰富一下想像力又有何益?

另一个故事。说一位博士设计了个CPU,能在o(n^7)--可能是7--时间复杂度内解
决某个问题。更牛的人判定,这个问题像是个NP难问题,也就是说,这个问题理
论告诉我们,不能在这种时间内解决。即1.这位博士创造了奇迹,他很快就要得
图灵奖了,整个计算机行业的数学基础也快要换了;2.他整错了。

在严格地论证以前,你更愿意相信哪个?

能尽早地判断美好地梦想不能实现,并承认这一点,非常重要。甚至比美好梦想
本身更重要。

判断那是美好梦想还是可能未来的一个重要的方法:你是否具有实现它的能力。

如何度量能力本身呢?

我并不太知道能力应该如何,不过,对于这种能力不是什么,我略有考虑,与你
分享。

实现梦想的能力,不应该是聪明和飞跃的。

《三宝大闹宝莱坞》里,编剧和导演为了表现主角是聪明睿智的,设计他完成了
以下任务:用照明灯的电惩罚向门上撒尿的学长、带无线摄像头的四旋翼直升机、
把危重病人绑在自己身上用两轮摩托车送去医院……用自行车带动(发电?)电推
子剪羊毛。各种聪明的做法。这些都很了不起,但是这些做法有个共同的特点,
使得它们的效用受限,那就是,这些做法不能用于设计航空母舰、航天飞机、导
弹,甚至AK47。我们在这些工业成果中,看到的是智慧,聪明无所置其身。

也许吧,印度和我们一样,作为第三世界国家,对于技术具有这样的幻想,我们
认为英雄可以拯救世界。而事实上,即使有一千个刺客,也不能攻下四平。孙子
说,以正合,以奇胜。没有正,奇是没有用武之地的。杀伐掠地,最终靠的还是
野战攻城,而非奇技淫巧。

《黑客与画家说》编程有三个层次:

: (a)使用一种强大的语言,(b)为这个难题写一个事实上的解
: 释器,或者(c)你自己变成这个难题的人肉编译器。

使用lisp语言,就是方案a;先写接口和函数库,或者写个dsl,大致相当于方案
b;用设计模式,或者把可能用编译器或者lisp macro生成的代码用手一行行写出
来,大致是方案c。

作者暗示我们,方案a是最高妙的,方案b等而下之,方案c最基础。

估计有很多人想,俺们应该按方案a行事。

问题是,如果你能够使用lisp macro的话,有一个前提,你本人必是编译器/解释
器的专家,才有能力运用这种高级的特性。利剑在一个居家的妇男手中,可能还
不如切菜刀好使。如果你想用方案b,写个解释器,除了要具备写解释器的能力
外(此时不需要具备lisp macro的运用能力,也不需要会lisp语言),有一个前
提,那就是你本人必须能像编译器生成代码一样用手把代码写出来。你只是不想
这么做,不是没有这样的能力。

你自己能完成的任务,才能指派别人去做。自己没有能力完成,却偏偏觉得别人
能够完成,而能够完成的人还不如他,这样的人也存在,不过大家面对他的时候
都感觉不太爽。

而很多人的问题是,代码写得尚且人和编译器都不忍卒读,又怎么可能写出解释
器,或者更高级地用lisp macro作为编译器生成出代码来再执行呢。

作者这样说,固然指出了一条光辉大道,但这大道必须从脚下走起。他只是没有
指出我们和他之间的鸿沟,估计会令很多人淹死在眼前的小河沟里。鸿鹄之飞固
然舒展而高级,不过虫子有虫子的移动之法,那就是爬。更何况同样飞得不错的
蝴蝶,当初爬得也是飞快的,虽然可能并不是最快。

这里,还需要小心,实现的能力,是指能切实操作的能力,而不是感觉似乎大概
可以。

能够和不能够,二者必居其一。"差不多"这种程度,根本就不存在。

我初中的学校,以前提过,是山上片,升重点高中比例不高。每年大约十多个。
也就是说,如果你不是在这十多个里面,你是25名,27名,又或者
100名,150名,在升重点高中这一点上,是没有什么差别的。

要么行,要么不行,"差不多"的内部,没有级别。而不行和行之间,有天渊之别。

大学刚入学的时候,我以为我的听力老好了,还去过外语系请教学长,应该怎么
才能学得更好。很多年以后我才知道,如果只是听个一知半解,再加上看字幕或
者根据故事情节猜测,那根本不能算是听懂。要知道,一句外语也不会,光靠表
情,我们就能跟老外聊个半天。

但是,你能告诉他你对于世界局势的看法么?你能精确地传达,斜上45度角倾望
天空是当今很多被另一些人称为脑残青年的生活态度,这样复杂的含义么?又或
者:

: Lisp functions take Lisp values as input and return Lisp values. They
: are executed at run-time. Lisp macros take Lisp code as input, and
: return Lisp code. They are executed at compiler pre-processor time,
: just like in C. The resultant code gets executed at run-time.

不能精确表达,我们就只能生存,而不能工作--工作,就是对别人有意义的事。
在这种情况下,你除了用父母的钱和微笑表达对别人的情感以外,对他人毫无帮
助。

其实,一个秘密,我们还有一个办法。

那就是,我们假装仍然生活在1500年以前的乡村,无论世界的哪个地方。我们施
施然穿过大街,任车流抚过身体,如同流水抚过舒蔓柔美的水草。

然后,我们要快乐的生活,全然忘记明天及以后。我们满足于我们的幻想,过
去,我们无比伟大,未来也是。

我们无论做什么,都"差不多"和最牛的那些想法非常接近了。只是,我们没有再
稍微努力一点,或者再精确一点,被他们占了先。但是,我们仍然是最聪明的个
体或者民族。只有哪天一发力,那世界就完全不同啦。

这很简单。只要我们忘掉,这世界就像编译器一样残忍,只要你写错一个字符,
那么,一切就都是错的。任你流着眼泪说,我不过就差一点点,它都无动于衷。
孩子,这也是一种生活。

20111218

pics

.

穷人如何使用测试驱动开发进行重构

穷人如何使用测试驱动开发进行重构

重构这个词现在已经被用烂了。我们经常听经理啥的说,咱们应该把系统重构一
下。当他用重构这个词的时候,想表达两个意思。一,把这个系统重写一遍,大
手术,二,我很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.txt

5. 当你需要测试驱动的时候,执行这样一条指令:

diff output expected.txt

如果啥结果也没输出,成功了。我第一次见到比较成功的时候没有输出,很惊度。
李记者教导我说,diff么,就是找不同,没有不同,它就应该不吱声。

如果有任何输出,你的程序仍然没有符合要求,继续改,继续跑。

6. 以上,是测试驱动开发。测试驱动开发,是重构的前提。没有测试驱动,一
次次用眼睛在程序输出里找你想要发现的错误或者期待的字符,你就是在构建性
能可怜而其实非常宝贵的人肉计算机。

如果不同太多,你又想找特定的字符,请使用 grep。

7. 以上的 diff 和 grep 是标准unix系统的小工具,所有的Linux版本里都有。
如果你使用windows,那么,可以下载 mingw & msys,或者 gnuwin
[http://gnuwin32.sourceforge.net/]。

8. 关于重构,补充一点。

想各种办法,始终保持约20分钟内,代码可以编译和运行和测试通过。尽可能不
要冒险长时间不编译或不运行或者测试不通过。你可能会期待在一个大~~~的不间
断的重构之后,代码一下子跑起来了,而且正常了。

这就像打算沿着海岸游泳到达某个地方,我们可能遇到一个海湾,抄近路似乎一
下子就过去了。你可以想像一下,从丹东到青岛,一种方案是沿着渤海湾一次半
公里,另一种方案是取直线。取直线,真的是极富有吸引力。毕竟,我们编程大
半是为了快感,而不是为了资本家。可是,如果你沉了呢,后果很严重。这也是
为什么需要测试驱动开发的原因。

小步的迭代,每步都要确保能活着再次接触海岸。我妈说了:宁走十步远,不走
一步险。

我曾经4个小时左右,不编译,不执行,不测试,这个期间程序就跟大手术尚未缝
合,根本不能跑。一下子跑起来,是的,非常兴奋。可是,也有几个小时以后完
成失败,甚至回滚到几分钟前都没有意义,只能一下回滚到几小时以前。那种挫
败感也令人印象深刻。这些经历,我知道你也有,就像牛仔的伤疤,如果没死是
大可以拿出来显摆的。

不过,还是每天写一点,每小时写一点,20分钟写一点。以微小的可控的单位前
进。像竹子那样,每走一步,就要做个小节。

未来无限美好,此刻却虚无飘渺。如果有经理老板导师男朋友女朋友对你这样
说,你就给他讲讲测试驱动开发和重构的道理。

----

完成所有今天能完成的任务,喝一杯咖啡,然后倒头大睡。人生当如此,哪怕就
此长眠,也了无遗憾。

20111216

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封住因特网,告诉我们只要看什么就能富国强民创新科技的时候,我就想
起了以上这些。

20111211

罗马十二铜表法的进步与软件工程需求与原型实验

罗马十二铜表法的进步与软件工程需求与原型实验

"十二铜表法就其阶级实质说,是一部分严格维护私有财产,维护贵族利益的法典。
然后,十二铜表法的颁为罗马法的发展奠定了基石,是整个罗马历史的发展过程
中的一座里程碑。尽管法律条文极力地对贵族的特权地位加以维护,例如,在第
十一表的内容中,强调了"平民与贵族不得通婚"。可是,制定法律并将其以文字
形式公布于众,本身就是法律史上的一个巨大进步。从此,国家对法律行为的有
效、无效、赔偿、处罚等都有了明确的规定,在一定程度上限制了贵族官吏的专
横。"

罗马:从共和走向帝制
宫秀华 著
高等教育出版社
2006年7月第2版

这个故事告诉我们,在软件工程中,清晰的需求,哪怕是错误的,也比模糊的需
求要好。甚至可以说,模糊的需求比没有需求更坏。

陈述观点的时候也是一样,清晰的观点,即使是错误的,也可以拿出来作为靶子
进行充分的讨论。

鲁迅先生说:水有的浅而清,有的深而浊,有的浅而浊,有的深而清.世间的人,大凡
可归此四种.宁可要浅而清的人,也不要深而浊的人.

所以,我们听完别人的观点,可以复述一遍:我理解的是否你的原意?

当我们读完教材或者文献,不妨做个实验,验证一下我们对于理论的理解是否正
确。这就是工程原型。如果原型是错误的,我们的代价相对较小,如果没有原
型,等到工程正式实施的时候,代价则可能是惨重的。

当我们操作真实世界的时候,由于要花费水泥沙子木料,所以我们相对谨慎。其
实,当我们操作虚拟世界的时候也是一样,因为我们正在花费更宝贵的东西,你
的时间。

尽早发现错误,则损失比更晚发现要少,因为如果错误保持下去,我们还将在此
基础上做很多工作,这些工作都将废弃。

尽早发现错误的一个方法就是,尽可能清晰地描述。

这就又回到了老问题上:你真正想要的,是什么?

20111205

使用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。
祝你开垦顺利,有收获。

使用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.stg

delimiters "$", "$"

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$();
};

20111203

使用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

20111202

使用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_c

peach:
stage_1 123 | stage_2

bowser:
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.cpp

3. 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行,收尾工作非常重要。项目的开始并非开始于项目开
始的时候,而是开始于上一个项目结束的时候。

所以,令我们安慰的,如果当前有什么事情无法完成,那不是此刻的你的错误,
而是更早的你积累的结果。非常简单的推论,如果未来的你有什么事情无法完
成,其错肇始于此刻。

20111201

使用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.h

1 #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