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

20111204

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

20111130

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

.

20111124

我想买个键盘:用户需求可以多么刁钻

我想买个键盘:用户需求可以多么刁钻

套用那句著名的"我想要的很简单",今天我想要的也很简单,我想买个键盘。

为了这个简单的愿望,不仅昨天在网上查了半个晚上,今天又跑了半天。这证明,所有的用户需求都是不简单的,包括某些看起来非常简单的。

只要这个要求是特异的,那么就不简单;如果这个要求不是特异的,它根本就不会存在。

如果我要求键盘上面有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,有时连多出来免费提供的也是问题,比如编辑区和数字键盘区。

还有的用户,像孩子一样,只会哇哇大叫,表达的是"我不舒服我不爽"。但是他需要的是什么,连他自己也不知道。这又让人有什么法子呢。

我们,就真的知道自己想要的是什么吗?如果你真的知道,为什么没有去做,没有尽力去做。

你真的想要的是什么?

20111118

没打游戏,我非常难过

没打游戏,我非常难过

雾气在窗玻璃上凝结了薄薄的一层,慢慢滑落,勾勒出几条平行弯折的河流。河
流的底下,是毫无亮光的浓黑,如同大地。

我是多么地想打游戏啊。

升到最高级的投石车轰开在森林里轰开一条小径,三五弓骑兵悄悄潜入敌人的基
地。突然间警钟响起,骑兵开始射杀农民矿工和猎手,远方的敌军驰援而回。另
一处,据河防守,城门铁闸升起,成群重装骑兵火速冲出,去砍断攻城器的木架。
攻击器收起退入戟兵丛中,追击的骑兵头上红缨如火般抖动。

在某一种森林,雾气升起,只看到林木的剪影,敌人隐藏其中。背后追击的,精
灵族满怀仇恨,因为我们收留了与我们同样身为人类的强盗,前方拦截的,不死
族蓄势已久,正等着截断我们的归路。我们是一群老弱残兵,不敢与任何强敌稍
加接触。我们结成人墙,把医生和法师围在当中。当遇到攻击,活下来的人听到
的是满耳的战友的悲号,但是没有一个人敢稍作停留反击。只有毫不停息,才可
能有人活下来回去通知故乡的人立即组织防御。

我不记得了。是不是每一次,当丛林散尽,终于能见到天日。看一条大河横在面
前,河上只有一桥,桥上是一名敌人的悍将。是不是每一次,欲哭无泪的时候,
都有一员使长枪的将军站出来大喊一声:我来断后。

应该是的,不然,我们怎么能够走出这么远。

"我来断后"的意思是:只有我在战线的最后,只有我与敌人接触。请你们,无论
谁也不要回顾、迟疑、救援。

一句话的意思,就是它本身的意思。而不是它背后隐含的意思。

下午听ZHUMAO报告,并参观机房。之前我提到:只许看啊,谁也不许碰。中间有
人靠近,有人碰了。

事后我说,这件事需要说一下,然后我说,我并不是批评某一位同学,因为有另
外一位同学也碰了。

其实我的表达不清晰。并非因为有别人也碰了,所以不批评你。我说不是批评你
的意思,就是不是批评。因为,我并有因此对你所做的事情也并未因此对你做出
任何否定。你之所以那么做,是因为你还不了解。不知者不为罪。

这里我又提起,是因为很多人在说"我不是批评你"的时候,她的意思恰恰是"我
就是在批评你"。我完全不是那样的人。我说不是批评的时候就是不是批评,如
果我的意图是批评,我也会直接说:我就是在批评你。

关于为什么不能碰机房的机器,我下午说过了,可能你也并非计算机专业的同
学,不必了解更多。只需猜想,不影响我接着扯。

前几天与编辑讨论,编辑大人说:秉笔直书,只管描述。

我说:如果我这样这样再这样描述,你觉得行么。

当然不行,那体现了作者错误的人生观价值观和...世界观(?)。俺们学校是有
要求的,要求教师不仅在学术上,也要在德育上教育学生。我真的得小声地说:
我道德水平本身就非常不咋地,怎么能够再在道德上教育学生。

下午跟ZHUMAO还提到,当然,我还算有点自知之明的。还有道德水平比我略高,
却远低于正确三观标准的,仍然坚持德育学生,这是正确啊正确啊还是正确的做
法。

我们怎么能够教育学生我们自己都不相信不执行的东西?

-----------------

描述就是描述,观点就是观点。捍卫观点,从来也不是问题,把观点伪装成客观
描述,就很成问题。

国人用词暧昧娓婉,语多曲折,不输于小鬼子。很多时候表面上看是出于尊敬,
其实多是伪装中肯。

前些日子某机构发文,要求某些同事去拍照。文件写的大意就是:这是给大家的
机会。

我写信询问,到底这是福利呢还是行政命令。如果是福利,我不喜欢,不想参加。
我没有说的是,如果是行政命令,我们来讨论一下管辖权。

明明是命令你的时候,偏要说:你看,这样做比较好,尤其是对你比较好。让我
想起多年前,老板们找你干活,偏偏要说这是对你能力的锻炼,给你创造机会。

-----------------

又想起下午在ZHUMAO那里提起的一件事。讲C++的时候,胡同学曾经在课程结束
后跟我要课件。我说,不行。后来考试结束了,胡同学说:老师,这回可以了吧。

我说,不行。

胡同学问:为什么呢。

我的回答是:因为那是我的啊。

我知道你可能猜测我不给课件的原因,是担心考试题目吧。不是。原因是...因
为那是我的,所以我可以不必给出原因而拒绝。

我只是把你和所有的组织机构上级领导一视同仁。我的,就是我的。那些完全属
于我的东西,除我以外,没有任何人可以用任何方式替我做任何决定。

你与组织机构上级领导是一样的。我与你,也是一样的。我与你一样,是多么地
想打游戏啊。

会有很多同学表扬胡同学的钻研精神,大加鼓励...最后以各种借口不能给出课
件。

-----------------

与某些同学所不同者,我没有用矫词伪饰,并以为那是对别人智商的侮辱。

你之于我,正如给笼子里的狗喂水的那位富妇人。无论言辞多么漂亮,举止多么
优雅和符合韵律,多少升同情的眼泪,在我的称谓前加上多少形容词顺便彰显你
的文化,总归不能脱出这样的事实:在你的眼里,我是狗--或者可资使用的物件。

加多少尊重、掩饰,对于本质也没有什么改变。

除此以外的情感,回忆里的美好,我希望能放在另一个时候来谈:当没有利害关
系的时候。

昨天读到《查太莱夫人的情人》的评论时注意到,那位男主人公说:很多人都没
有得到,但是他们假装得到了;如果我没有得到,我决不能欺骗自己说得到了。

决计不能欺骗自己。我非常想打游戏,我没有打,我非常难过。

20111116

教学相长,和我的学生

教学相长,和我的学生

今天下午被要求听讨论教学的会。高哥提到两句对我非学有启发:一句是教学相
长。这篇日志都是关于这个的。另一句是,马克思说,人的创造力在于他的自由
时间。自由--我努力追求能让内宁静的自由。

以下是教学相长的。

1. 大毅同学

一大清早去单位,有讨论。

趁正题开始前,我画了半个白板的某架构或者工具链。又一次燃起雄心壮志整那
个难整的东西。绘图控件。

很多年前,刘同学就开始和我整这个,用了好几种编译器生成器。到现在,那些
中间结果都没有用上。而他已经要毕业了。他和建一一起提到,这世界,更多的
需要做web前端的什么的。

我坚定地相信,底层,或者说深入核心的部分,仍然需要有人去做,而且一定是
我们。我们不跟踪时尚,我们固守那些基本的原则,并不断深入探索。

但是,我不敢去触动做了好几年也不敢整的绘图控件。按与包师弟谈时我提到
的,我甚至不敢开始。

刘同学说:你找个本科生,就光做这个呢?

他指的是本科中的强人,我们都知道指的是谁。

我说:比如那位某某吧。我舍不得啊。

他说:做不出来就算了呗。

我说出我的顾虑:那学生不就毁了么。

我想的是多年前,我们做了好几年的编译器生成器。那些没有成果的工作。心怀
愧疚。

刘说:其实做不出来,学生也有收获。

后来同学们问我,为什么看起来很累。我说:我晚上1点多睡的。又说,一大早
KFC关门,没喝成咖啡。

这些都不是原因,我在想刘同学的话。也许你们是真的对咱们啥也做不出来的那
些工作心无抱怨,但是我却不能没有遗憾和抱歉。

糟烂工作之所以糟糕的原因,正应该归疚于导师或者项目组长或者技术负责人。

也许吧,刘同学说的对,即使我们什么也没做出来,却仍然有收获。


2. 建一同学

建一同学也快毕业了。前一段公安厅的项目,请他帮助我指导齐同学,齐同学甚
有收获。

我后来问建一,你咋整的呢。

我之所以问,是因为我经常把同学们整得JJWW的,感觉我逼得很紧太过严厉,同
时我经常觉得这些简单的问题都应该你们自己整,咋能啥都指着我手把手教呢。

我问建一,你咋整的。齐同学出成果太快,大大出乎我的意料啊。我记得是期待
一周,结果三天左右吧。

当时的任务是写个GUI,把我写的业务逻辑包起来。齐同学将在这里看到项目中
的分工,接口,写代码的经验。我希望建一不亲自参与写代码。

建一把任务分成了三步,让齐同学去做。第一步是建个简单的GUI,啥消息响应也
没有;第二步是添加一些控件,就是我在项目里要求的那些,图片label,文字
label等等;第三步是把GUI套在我的业务逻辑之外,调用我提供的函数。

于是事情就成了。

我想起了袁师弟以前教我RESET笔记本。

我说:这破玩意根本从来就不好使。不是按4秒就重启么。

袁说:你不能这么数4秒,1234,你得这么数,1--2--3--4。

他按住reset,于是机器重启了。而我此前从不能成功。

我太缺乏耐心,不能等待各种反应缓慢地发生。

当年家教的时候,那学生的妈妈说孩子挺聪明的。我就信了。我讲完基本概念和
解法以后,问:你明白了没?

明白了。

我从书后找道难度还可以的题目,他不会。

很多年以后,二猫妈提醒我:你得整一道跟那原理完全完全相同的题目,让他有
成就感。

我说:那不是侮辱他的智商么。

我缺乏耐性如此,而我的学生们一直一直忍耐着我,等我明白。

而且,你们教会我:

2.凡是不能一蹴而就的任务,其中好多可以分解成一个个阶段,依次完成。

那些无法划分阶段,或者每个阶段我们仍无法完成,或者穷我们毕生也无法完成
这些阶段的,就是不能完成的。对于不能完成的,我们又有什么遗憾的。

而且,1.即使一无所成,我们仍有收获。

人生,正是如此。谢谢教导,敬受教。

20111111

牛人与弱手的区别,不是线性的,而是指数的 及他们之间的道路

牛人与弱手的区别,不是线性的,而是指数的 及他们之间的道路

1.误操作

今天傍晚,我删除了整个下午的工作成果。误操作。执行的是那个著名的指令
"rm * -rf"。

我发了会呆,再三确认是这个目录,然后手悬在键盘上不知道该敲些什么。我跑
到GOOGLE,没有解决方案。牛同学说,能不能恢复啊?

我说:不能,十好几年前我就知道不能恢复。

这命令太著名,且令人印象深刻了。上网查一下,只是安慰一下自己。恩,我并
不孤独,傻子真多啊。

有多少人有这样的经历呢,先是对着心爱的人喊"我再也不想见到你",当如愿以
偿的时候,开始对着空虚哭泣。

rm是remove,-f是不必再问一次,-r是递归。递归的意思是,把目录下的目录下
的目录下的...东西,都照此处理。类似于,我烧掉你的信,烧掉提到你的信的
日记,烧掉提到日记的作文,烧掉提到作文的...我想你明白了。

大半夜的,刚又调了半宿,我可不是跑这伤感来了。当时,我对着屏幕发了一会
呆,然后对张宇同学说:你回家吧,今天的观摩到此结束。最重要的内容你刚刚
学到了,今天学不着啥了。

然后,我开始键入,跟张健一边回忆下午的全部工作。我们花了大约1小时20分
钟,把整个下午的工作完成了。接下来的半个晚上,我证明了这1小时20分中,
大部分时间是在试图解决一个暴露出的新问题,而不是恢复下午的工作。

一整个下午 对比 1小时20分,其实这正是令人悲哀的差距。

1小时20分 说明,我们,具体地说就是我,一整个下午,大部分工作是在发现错
误和改正错误,而真正的工作,1小时20分钟足以完成。

这就是高手与我们这样的低手的差别:我们引以为豪的一整个下午的工作,对于
不犯那些愚蠢错误的高手而言,只值1小时20分。

2.效率之差

高手通过避免错误节省时间,从突显出他的效率。即使犯了错误,他也能更快地
发现错误的原因,也更快地修正错误。

想起ACM比赛,牛人和面人之间的差距,岂可以道里计。相信有过比赛经验的同
学深有体会。在工程中,面对牛人深感绝望的各位也一定深有同感。

更可悲哀的是有一些同学尚无法看到自己与牛人的差距,动辄认为:我也能整出
来。

他所看到的,是现实或者电影里牛人几乎不犯错误的流程,并投射到自己,却不
知道 不犯错误本身正是至难的事情;犯了错误能够以别人几乎无法发现的速度
发现和更正,也并非人人能够。

我们把大部分时间都花费在犯错误上了。

骄傲的同学认为:其实再给我一点时间,我也可以。

时间。这个世界最吝惜给予我们的,恰恰就是时间。

如果我们有足够的时间,我们就可以在战场上迂回到敌人的后方;如果我们有足
够的时间,我们就可以比对手更早地把刀切在某个部位;如果我们有足够的时
间,我们就能把这些兵那些兵都调到敌人的基地,把所有的矿全占了……

所有这些愿望,只需要一个前提,那就是当我们拥有时间的时候,别人没有同样
拥有。

你这为这个前提可能实现么?

时间。这个世界最吝惜给予我们的,恰恰就是时间。所以,我们在战场以下用小
时计算以天计算以年月计算的时间,却换取战场上的几秒钟。

我们用这些时间训练自己避免错误,也训练自己迅速改正错误。这样,在战场
上,我们就可以有更多的时间做正确的事。

经常有年轻人抱怨,失声痛哭,为什么结果会是这么悲惨。为了让你自己好受一
些,请假设我在讨论的是中国男足。其实结果如此悲惨的原因非常简单而直接,
在过去所有的岁月中,你做出了这样的选择。

我们与牛人能有多大的差距,在甚至不足一生的努力之后?

想想你和你的小学同学现在的差别,和你的高中同学,和你的大学同学。有一些
人,如偶像YMH,对够达到令我认为 对比是没有意义的,因为永远不可能达到,
更遑论超越。

据说巨牛的程序员的效率是面手程序员的10倍。我想,做出这个统计数字的人,
一定没有参观我们的教室和实验室。

3.图书馆案例

正确的方法与糟烂的方法效率之差能有多大?一个真实的案例,但是今天不细说。

当年在图书馆导数据出来。

预定的方案导出全部书目所需的时间是:200个工作日左右,每天8小时,需要一
位工作人员陪着机器。

我们经过1个月还是一周的研究,最终的方案导出数据花费了:半小时,人类按
键花费了不到1分钟,然后就离开了。

如果我们更牛,避免错误技术路线的尝试,这两个方案的对比就是 200天
vs. 30分钟。

4.于同学的问题

周三李记者讲座以后,于同学留言问到:

"其实老师,我还有一个问题没有问(我怕老师们回答不了):如果一个人很热爱
代码,很喜欢计算机,但是他学的就是不好,编程就是不如别人,他还有存在的
价值吗,他如何生存(可以译为小人物的生存之道)。习编程。"

其实这个问题一点也不难回答。初步的回答是:

"我唱歌跑调挺严重的,但是我学习弹琴;我体质挺差的,但是我坚持做俯卧撑--我
一直在进步。結果并非总是重要的,过程也是我们所追求的;试想,人生如果忽
略过程,只看結果,还有什么意思。"

只要努力总会进步,而且进步是成指数,而不是线性的。牛人,一般都是从面人
过来的,当年也都面得可笑。

很难想像热爱却不能精通,如果你用一生的时候去追求。

是的,由于天份所限--我们不得不承认--直到生命最后一刻,我们也没有成为巨
牛的牛人,但是你的高度已经可以被很多人仰视了。你三两岁的时候,如果像现
在这样畏难,一定会被自己当时走路和语言的能力吓得每天哇哇大哭。

我知道,你当时确实是那样做的,每天哇哇大哭。现在,请别再以另一种形式继
续哭了。

而且,我们的人生并非用来与人比较,这一进步的过程,也正是我们所享受的。

我们终无可能踏入神的队列,但是我们从未停止努力的脚步。正是持久的努力之
路,而并非结果,令我们不同于凡俗。

20111109

2011年11月9日,我的理想

2011年11月9日,我的理想

2011年11月9日,历史从这一天改写了。

李记者下午讲座,问同学们,以后想干啥。王同学说:想做像杨老师那样的人。

我非常激动。极其激动。

当年去丹麦和Lars闲扯。他问我,你想你的学生们么?我说:想。他给我看大家
的照片,我愈加想念。Lars说你的学生都挺喜欢你的。我说:啊。L

又经过了若干谈话,Lars说:做一名教师,应该能够让学生希望将来成为自己这
样的人。

今天,当我听到王同学这样说的时候,我想起了Lars的话,想起了几次中丹合作
项目,回想起我们成功和失败的项目。

人生不如意十有八九,今天这算是人生的一点亮色。偶而,我做教师也有成就感
的时候啦。

领导和上级的评价不足为凭,学生喜欢老师扯淡吹牛讲笑话也不足为据,学生N
多年以后后悔或感谢你当年的教诲也算不了什么。但是,我的学生想成为像我一
样的人。谢谢,我很自豪。

----

晚上吃饭,提到风投啊,就业形势越来越好啊,什么的。好象我们已经整到了很
多钱,可以大把地花了。

我说:我要把那些钱都发了。每个学生每个月四十万。每个人再发条狗,有男朋
友女朋友的都带着,每天就是去大连海边遛狗。没对象的,一人发一个对象。男
生发个女朋友,女生发个男朋友。

恩,如果有男生要求男朋友的,也可以满足。

考虑到中国的男女比例,我在女生要求女朋友这犹豫了一下。后来想想,也行。