2009年1月19日星期一

其实我买票也很难

转载的文章,原文请看这里

前两天,铁道部发言人说:“其实我买票也很难”,这句弥天大谎,当时笑抽了无数的网友。我记得“俯卧撑”之说刚出现时,有网友悲哀地说:“这些杂种以前骗我们的时候,还会精心编造谎言,但现在他们连精心编造谎言的耐心都没有了,因为他们根本就不在乎公众相不相信。”如果有必要,他们可以一脸真诚地对公众如是说:“王八其实是在天上飞的动物。”

  经常有人对铁路提出这样的质疑:“提前十天售票,我十天前的零点第一个出现在铁路售票窗口,却被告之无票,这是为什么?”为什么?原因很简单,这些紧俏的票,都被铁路直接截住了。但他们对外解释说:“现在全国联网售票,同一车次同一天的车票,全国所有车站都在同时卖,所以就算你第一个排队,也可能买不到票。”——你会相信这种解释么?的确,现在是微机联网售票,但微机掌握在谁手里?指望受人掌控的微机反过来控制操作微机的人,这可能么?如果你不相信,我现在就给你提供一个具体的地点,你去那里调查一下,就会发现非常奇怪的事情:在哈尔滨铁路局机关后面的邮政街上,与局机关大楼一体有个铁路售票点,来这个售票点的人大多都是拿批条取票的人,条子往里一递,如是说:“姐,把这五张票给打出来。”里面的那老姐在电脑上叭叭叭一敲,那几张指定车次且指定铺位的车票就如愿而出了。而这些车票,却大多都是在正常的售票窗口第一时间就被告之“无票”的。

  至于这些人是通过什么办法把应当正常出售的客票截住另售的,那就不得而知了。可以算一笔帐:缩小下范围,只计算铁路局机关:全国铁路,有多少个局机关干部?他们哪个人没有亲戚朋友?谁没给亲戚朋友买过这种特权票?另外,亲戚朋友还有亲戚朋友,如此计算一下,那将是个惊人的数目,我这还没有计算各个铁路车站的高层管理者,如果算上这些次级别的特权者,数量将会更惊人。而列车的数量是有限的,那么,加开多少次列车,能添满这个无底洞呢?就算你提前一年到售票窗口排队,又有什么用呢?

  当然,铁路的运能的确紧张,虽然运量不断增加,但客流上升的速度却远比运量提升的快。因此一票难求的现象短时间内还是无法缓解。有人提出各种解决办法,但也仅是治标不治本的措施,比如实名制售票。就算是如愿实行实名制售票,这也仅仅是想要个公道而已:大家都正常买票,买到的算幸运,买不到的算倒霉,心理也不会有那么大的不平衡。因为在当前运能不变的情况下,车票的数量是有限的,无论你用什么方法出售,最终发送旅客的数量还是那么多。

  以前我写帖子曾经提到过实体公正和程序公正的问题。现在重提一下,罗尔斯分析说:程序公正虽然无法确保实体公正,但公正的程序却能让或公正或不公正的实体以一视同仁的方式运行。这样说有些拗口,但举个例子就很好理解,比如实体法规定说:“偷东西的价值超过一块钱不满五块钱的,剁右手。”从实体的角度分析,这绝对是不公正的,一块钱和一只手的价值没法相比。但是,假如这个不公正的实体在公正的程序法的规范下,公正的执行,却也能消除人们心理上的一些不平衡,多少化解实体的不正义。比如今天社会上的无名小瘪三偷了一块钱,手被剁了;明天市长的儿子偷了一块钱,手也被剁了。但现实是:无名小三偷了一块钱,被剁了手,市长的儿子偷了几个亿,却当了整邪委员。

  做为铁路来讲,实体上应当做的,是提高铁路运量;而程序上的当务之急,应当是消除售票环节中的猫腻。哪怕仍然一票难求,但假如真能做到售票环节公开透明,百姓也不会有那么大的怨气。这两天的新闻不断播出采访铁路领导的画面,什么要求窗口售票员戴胸卡上岗啦、到位后马上把窗帘拉开啦、什么今天又抓住多少票贩子云云。我一针见血地说,这些,都是上坟烧报纸,骗鬼。窗口的售票员根本就没办法在票上捣鬼,真正的大鬼,是这些售票员上级的上级,他们躲在旅客永远看不见的安全地点。另外,一看到铁警满大街抓票贩子我就想吐。这些票贩子固然可恶,因为他们截取应当正常出售的车票,从急于出行的旅客那里赚黑心钱,但是,比这些票贩子更可恶的,是谁?比如我前面提到的那个哈尔滨铁路局机关大楼后身的售票点,为什么就没有人抓住其中一个拿批条的人问问:“你这个条子是谁批的?他为什么能批到正常售票窗口买不到的车票?”追根溯源一下,这些有权批条子的人与票贩子,哪个更可恶?

  一幕幕贼喊抓贼的游戏,在每年的春运上演,而表演者就是铁路管理者本身。“其实我买票也很难”,这话竟然是铁路高级官员说的。我建议把这句话列为2009年最雷人的笑话之首。

2009年1月14日星期三

我将若无其事地归来开放

转载一下老罗的博客,支持牛博。

2009年1月9日下午3时许,收到一封来自万网客服的邮件,内容如下:

您好!

北京市通管局下发北京市政府新闻办公室通知:www.bullog.cn站点大量登载时政类有害信息,已要求该网站整改,但该网站仍未采取有效整改措施。

现需要万网记录停止该域名的访问权限;

HOLD域名bullog.cn;

(原信如此,到上面的那个分号就没下文了)

收到这封信后,过了一会儿,又接到我们的服务器托管商的客服电话,说机房接到通信管理局的通知要求关闭牛博服务器,我抢在最后的时刻用Paparazzi! 这个软件做了截图,留了个纪念。可惜牛博的版式在safari浏览器(Paparazzi!好像只支持safari)下显示不太好看,footer甚至显示出错。

(照片太大,影响速度,刚才暂时取消了。)

由于无聊,我和黄斌在牛博网被关闭后,就我们俩谁表现得更平静进行了幼稚的攀比,结果是我觉得我们俩一样平静,他认为他表现得更平静。从这个结果来看,我在做人方面,生平第一次胜了黄总一个回合^_^。建议黄总看得淡一点儿。

牛博关掉只不过半个多小时,就接到了美帝国主义的反动媒体时代周刊的记者电话,在此向时代周刊的高效率致敬。按照我一贯不接受外国反动媒体采访的做法,我对该反动记者表示了拒绝回答任何问题,除了确认被关闭的事实。

又过了一会儿,一个同事对着电脑惊呼“我操!”,我们围上去一看,发现维基百科上的“牛博网”词条里多了一句“2009年1月9日下午,牛博网国内服务器被关闭,牛博国际目前也无法登陆...”我们集体惊呼“我操!太牛逼了!这速度...”在此向伟大的维基百科的高效率致敬。

接下来又来了几个外国反动媒体的电话,我照样拒绝了采访。美联社的记者助理极差的业务素质(“请问你们是牛什么网来着?”)和英国卫报的体贴(“我们可以对外讲是您本人承认了牛博被关闭的事实吗?”)都给我留下了深刻的印象。

晚上回家发现www.bullog.cn这个域名已经被绑架到了一个“低俗”网站上,见下图:


我认为在当前政府紧锣密鼓地进行“反低俗”的大气候下,把一个完全没有低俗内容的(在同样流量规模的网站里,牛博好像是唯一一家没有“低俗内容”的中国网站了)牛博网的域名公然绑架到一个充斥着“胸模自述丰满秘籍”,“锁住男人的眼球,打造勾人魂魄的迷人乳沟”,“女性“紧窄”-老公最爱”,“做爱时阴道咣当咣当的,宽得就像大操场--阴松;做爱时阴道猛“放屁”,性致头上浇冷水--阴吹;内阴外阴比虱子抓还痒,性生活后腰腹疼--阴毒;阴道干涉得像河床干裂,性爱变成撕裂痛--阴干”之类的内容的网站上,是对党和政府的悍然挑衅。我在此嫣然建议有关部门从严从重追究此事。

早晨醒来发现一口气睡了八个多小时,很久没连续这么睡过了,本来想给黄斌打个幼稚电话吹牛逼说我在牛博被关闭的当天夜里美美地睡足了八个小时,又怕这小子接电话的时候假装还没睡醒就比我更牛逼了,只好算了。拿起设成了静音的手机一看,有一大堆陌生人和老朋友的安慰短信,陌生人的一律回了个“谢谢”,老朋友的一律回了个“行了行了!都第四次了还这么大惊小怪的,该干嘛干嘛去。”

高高兴兴地牛逼完了起床后,我又陷入了深深的忧虑和自责:没了牛博,从今天开始,人民看什么?!你要是不重开牛博,人民会答应吗?!你倒是睡得舒坦了,你有没有想过睡不着的人民怎么办?!怎么办?!!怎么办?!!!

我在“怎么办”的回声中,带着浓浓的社会责任感毅然打开电脑,到百度和google的博客搜索里看了看网友的言论,发现关心牛博的朋友们的帖子里充斥着“哭泣”、“悲伤”、“悲愤”、“想哭”、“郁闷”、“痛心”、“祭牛博”、“黯然”、“泣告”、“伤感”、“so sad”之类的字眼。

唉,这帮没出息的东西,只好把我过去压箱底的必杀技热血青春励志故事再搬出来讲一次了(这是我听我年轻时期的偶像李敖老师讲的)。

刘秀带兵打解放战争的时候,有一次吃了大败仗。夜里刘秀巡视营房,发现东汉解放军的军官和士兵们都很消沉,有唉声叹气的,有哭的,有用阿杜的唱腔唱“擦干眼泪陪你睡的”,还有练毛笔字的(当然写的都是什么“郁闷”、“痛心”、“祭牛博”、“黯然”、“泣告”之类的破字儿),只有一个人,坐在营房里不动声色的擦各种武器(七种?),准备第二天的战斗。刘秀说,这小子牛逼,能成事儿。李敖老师教导我们说,面对挫折的时候,不要哭天抹泪,不要咒骂抱怨,不要悲观消极,要像这个人一样,埋头擦亮你的枪,准备下一次的战斗。

——————另起一行(一定要另起一行哦,效果不一样哦)——————

这个人就是大将军吴汉。

故事讲完了。

接下来说说昨晚的故事吧,要是昨晚有人刘秀附体到我家和黄斌家巡视,会发现我在家呼呼大睡,大摆混不吝的pose兼养精蓄锐,黄斌呢?正常熬夜(黄总很少不熬夜)做下一轮的牛博技术升级,抽空还随手抓了几个系统的小bug。

——————————————————————

最后严肃地说点严肃的:

牛博肯定会重开,这个不用担心,实在不行我就开一个海外的牛博国际,国内换个名字再开一个,可以叫驴博网。这已经说过很多次了。

在牛博开博客的用户们请放心,牛博上的文章我们都有备份,所以不用担心你在牛博的文章会丢失,等到牛博重开的时候自己认领就可以了。这也说过很多次了。

我个人不会有事,我只不过是办了一个让删帖就删帖,让删评论就删评论,让删整个博客就删整个博客的网站而已,不可能出什么事,大家不用自己吓唬自己了。

感谢德赛公园的来信,感谢所有关心牛博的朋友。

2009年1月13日星期二

ORACLE11g的几个新特性(针对开发者)

一 pivot操作符
这个东西应该是从sql server里学过来的,不过话说回来,这个功能的确很好用。说白了,就是做行转列。举个例子说明吧。要运行以下的例子,必须要有scott账户下的emp和dept两个表,如果没有的话,可以运行以下语句:
create table emp as select * from scott.emp;
create table dept as select * from scott.dept;

通常来说我们做行转列都是使用以下做法(在做报表的时候):
select deptno,
sum( decode( job, 'CLERK', sal ) ) clerk,
sum( decode( job, 'SALESMAN', sal ) ) salesman,
sum( decode( job, 'MANAGER', sal ) ) manager,
sum( decode( job, 'ANALYST', sal ) ) analyst,
sum( decode( job, 'PRESIDENT', sal ) ) president
from scott.emp
group by deptno
order by deptno;

这个查询得到的结果如下:
DEPTNO CLERK SALESMAN MANAGER ANALYST PRESIDENT
---------- ---------- ---------- ---------- ---------- ----------
10 1300 2450 5000
20 1900 2975 6000
30 950 5600 2850

而在11g中,我们可以这么写查询:
select *
from (select deptno, job, sal
from scott.emp ) e
pivot( sum(sal) for job in
( 'CLERK', 'SALESMAN', 'MANAGER',
'ANALYST', 'PRESIDENT' ) )
order by deptno;

更好的是,我们现在可以不用指定列表:
select *
from (select deptno, job, sal
from scott.emp ) e
pivot( sum(sal) for job in
(select distinct JOB from emp))
order by deptno;

二 sequence
在11g以前,在PLSQL里使用sequence的值必须用使用这种方法:
select t_seq.nextval() into var from dual;
而11g里可以直接赋值:
var:=t_seq.nextval();

三 log_error脚本
这是ORACLE提供的一个记录错误的日志脚本,特别适合在PLSQL编程的时候碰见WHEN OTHERS THEN。

CREATE OR REPLACE FUNCTION getosuser RETURN VARCHAR2 IS
vOSUser user_users.username%TYPE;
BEGIN
SELECT osuser
INTO vOSUser
FROM sys.v_$session
WHERE sid = (
SELECT sid
FROM sys.v_$mystat
WHERE rownum = 1);

RETURN vOSUser;
EXCEPTION
WHEN OTHERS THEN
RETURN 'UNK';
END getosuser;

CREATE TABLE errorlog (
procname VARCHAR2(61),
loadfilename VARCHAR2(40),
runtime DATE DEFAULT SYSDATE,
osuser VARCHAR2(30),
mesgtext VARCHAR2(250));

CREATE OR REPLACE PROCEDURE log_error (
pProcName VARCHAR2,
pLoadFile VARCHAR2,
pMesgText VARCHAR2)
IS

PRAGMA AUTONOMOUS_TRANSACTION;

BEGIN
INSERT INTO errorlog
(procname, loadfilename, osuser, mesgtext)
VALUES
(pProcName, pLoadFile, getOSUser, pMesgText);
COMMIT;

-- No exception handler intentionally. Why?

END log_error;
/
当然这个脚本也是自治事务的一个好例子。

以下是ORACLE提供的另外一个处理异常的脚本:
CREATE TABLE error_log (
error_timestamp TIMESTAMP(9),
database_name VARCHAR(50),
instance_number NUMBER,
error_number NUMBER,
error_message VARCHAR2(255),
logged_on_as VARCHAR2(30),
client_host VARCHAR2(50),
service_name VARCHAR2(30));

CREATE OR REPLACE PROCEDURE error_trap IS
odbname VARCHAR2(50); -- Oracle database name
oinst NUMBER; -- Oracle instance number
enum NUMBER; -- Error Message number
emsg VARCHAR2(250); -- Error text
curschema VARCHAR2(30);
clihost VARCHAR2(50);
serv_name VARCHAR2(30);

-- PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
enum := sqlcode;
emsg := sqlerrm;

odbname := ora_database_name;
oinst := ora_instance_num;

SELECT sys_context('USERENV', 'CURRENT_SCHEMA')
INTO curschema
FROM dual;

SELECT sys_context('USERENV', 'HOST')
INTO clihost
FROM dual;

SELECT sys_context('USERENV', 'SERVICE_NAME')
INTO serv_name
FROM dual;

INSERT INTO error_log
(error_timestamp, database_name, instance_number,
error_number, error_message, logged_on_as,
client_host, service_name)
VALUES
(SYSTIMESTAMP, odbname, oinst, enum, emsg,
curschema, clihost, serv_name);
COMMIT;
END error_trap;
/

CREATE OR REPLACE TRIGGER error_trig
AFTER SERVERERROR ON DATABASE
CALL error_trap
/

BEGIN
RAISE zero_divide;
END;
/

CREATE TABLE errors (
module VARCHAR2(50),
seq_number NUMBER,
error_number NUMBER,
error_mesg VARCHAR2(100),
error_stack VARCHAR2(2000),
call_stack VARCHAR2(2000),
timestamp DATE);

ALTER TABLE errors
ADD CONSTRAINT pk_errors
PRIMARY KEY (module, seq_number)
USING INDEX
TABLESPACE indx_sml;

CREATE TABLE call_stacks (
module VARCHAR2(50),
seq_number NUMBER,
call_order NUMBER,
object_handle VARCHAR2(10),
line_num NUMBER,
object_name VARCHAR2(80));

ALTER TABLE call_stacks
ADD CONSTRAINT pk_call_stacks
PRIMARY KEY (module, seq_number, call_order)
USING INDEX
TABLESPACE indx_sml;

ALTER TABLE call_stacks
ADD CONSTRAINT fk_cs_errors
FOREIGN KEY (module, seq_number)
REFERENCES errors (module, seq_number)
ON DELETE CASCADE;

CREATE TABLE error_stacks (
module VARCHAR2(50),
seq_number NUMBER,
error_order NUMBER,
facility CHAR(3),
error_number NUMBER(5),
error_mesg VARCHAR2(100));

ALTER TABLE error_stacks
ADD CONSTRAINT pk_error_stacks
PRIMARY KEY (module, seq_number, error_order)
USING INDEX
TABLESPACE indx_sml;

ALTER TABLE error_stacks
ADD CONSTRAINT fk_es_errors
FOREIGN KEY (module, seq_number)
REFERENCES errors (module, seq_number)
ON DELETE CASCADE;

CREATE SEQUENCE error_seq
START WITH 1
INCREMENT BY 1;

CREATE OR REPLACE PACKAGE ErrorPkg AS

/* Generic error handling package, using DBMS_UTILITY.FORMAT_ERROR_STACK and DBMS_UTILITY.FORMAT_CALL_STACK. This package stores general error information in the errors table, with detailed call stack and error stack information in the call_stacks and error_stacks tables, respectively.

Entry point for handling errors. HandleAll should be called from all exception handlers where you want the error to be logged. p_Top should be TRUE only at the topmost level of procedure nesting. It should be FALSE at other levels.
*/

PROCEDURE HandleAll(p_Top BOOLEAN);

/*
Prints the error and call stacks (using DBMS_OUTPUT) for the given module and sequence number.
*/

PROCEDURE PrintStacks(p_Module IN errors.module%TYPE,
p_SeqNum IN errors.seq_number%TYPE);

/*
Unwinds the call and error stacks, and stores them in the errors and call stacks tables. Returns the sequence number under which the error is stored. If p_CommitFlag is TRUE,
then the inserts are committed. In order to use StoreStacks, an error must have been handled. Thus HandleAll should have been called with p_Top = TRUE.
*/

PROCEDURE StoreStacks(p_Module IN errors.module%TYPE,
p_SeqNum OUT errors.seq_number%TYPE,
p_CommitFlag BOOLEAN DEFAULT FALSE);

END ErrorPkg;
/

CREATE OR REPLACE PACKAGE BODY ErrorPkg IS

v_NewLine CONSTANT CHAR(1) := CHR(10);

v_Handled BOOLEAN := FALSE;
v_ErrorStack VARCHAR2(2000);
v_CallStack VARCHAR2(2000);

PROCEDURE HandleAll(p_Top BOOLEAN) IS

BEGIN
IF p_Top THEN
v_Handled := FALSE;
ELSIF NOT v_Handled THEN
v_Handled := TRUE;
v_ErrorStack := DBMS_UTILITY.FORMAT_ERROR_STACK;
v_CallStack := DBMS_UTILITY.FORMAT_CALL_STACK;
END IF;
END HandleAll;
--===================================================
PROCEDURE PrintStacks(
p_Module IN errors.module%TYPE,
p_SeqNum IN errors.seq_number%TYPE)
IS

v_TimeStamp errors.timestamp%TYPE;
v_ErrorMsg errors.error_mesg%TYPE;

CURSOR c_CallCur IS
SELECT object_handle, line_num, object_name
FROM call_stacks
WHERE module = p_Module
AND seq_number = p_SeqNum
ORDER BY call_order;

CURSOR c_ErrorCur IS
SELECT facility, error_number, error_mesg
FROM error_stacks
WHERE module = p_Module
AND seq_number = p_SeqNum
ORDER BY error_order;

BEGIN
SELECT timestamp, error_mesg
INTO v_TimeStamp, v_ErrorMsg
FROM errors
WHERE module = p_Module
AND seq_number = p_SeqNum;

-- Output general error information.
dbms_output.put_line(TO_CHAR(v_TimeStamp, 'DD-MON-YY HH24:MI:SS'));
dbms_output.put(' Module: ' || p_Module);
dbms_output.put(' Error #' || p_SeqNum || ': ');
dbms_output.put_line(v_ErrorMsg);

-- Output the call stack.
dbms_output.put('Complete Call Stack:');
dbms_output.put(' Object Handle Line Number Object Name');
dbms_output.put_line(' ------------- ----------- -----------');

FOR v_CallRec in c_CallCur
LOOP
dbms_output.put(RPAD(' ' || v_CallRec.object_handle, 15));
dbms_output.put(RPAD(' ' || TO_CHAR(v_CallRec.line_num), 13));
dbms_output.put_line(' ' || v_CallRec.object_name);
END LOOP;

-- Output the error stack.
dbms_output.put_line('Complete Error Stack:');

FOR v_ErrorRec in c_ErrorCur
LOOP
dbms_output.put(' ' || v_ErrorRec.facility || '-');
dbms_output.put(TO_CHAR(v_ErrorRec.error_number) || ': ');
dbms_output.put_line(v_ErrorRec.error_mesg);
END LOOP;
END PrintStacks;
--===================================================
PROCEDURE StoreStacks(p_Module IN errors.module%TYPE,
p_SeqNum OUT errors.seq_number%TYPE,
p_CommitFlag BOOLEAN DEFAULT FALSE)
IS

v_SeqNum NUMBER;
v_Index NUMBER;
v_Length NUMBER;
v_End NUMBER;
v_Call VARCHAR2(100);
v_CallOrder NUMBER := 1;
v_Error VARCHAR2(120);
v_ErrorOrder NUMBER := 1;

v_Handle call_stacks.object_handle%TYPE;
v_LineNum call_stacks.line_num%TYPE;
v_ObjectName call_stacks.object_name%TYPE;

v_Facility error_stacks.facility%TYPE;
v_ErrNum error_stacks.error_number%TYPE;
v_ErrMsg error_stacks.error_mesg%TYPE;

v_FirstErrNum errors.error_number%TYPE;
v_FirstErrMsg errors.error_mesg%TYPE;

BEGIN
-- Get the error sequence number.
SELECT error_seq.nextval
INTO v_SeqNum
FROM dual;

p_SeqNum := v_SeqNum;

-- Insert first part of header info. into the errors table
INSERT INTO errors
(module, seq_number, error_stack, call_stack, timestamp)
VALUES
(p_Module, v_SeqNum, v_ErrorStack, v_CallStack, SYSDATE);

/*
Unwind the error stack to get each error out by scanning the
error stack string. Start with the index at the beginning of
the string
*;
v_Index := 1;

/*
Loop through the string, finding each newline
A newline ends each error on the stack
*/
WHILE v_Index < LENGTH(v_ErrorStack) LOOP
-- v_End is the position of the newline.
v_End := INSTR(v_ErrorStack, v_NewLine, v_Index);

-- The error is between the current index and the newline
v_Error := SUBSTR(v_ErrorStack, v_Index, v_End - v_Index);

-- Skip over the current error, for the next iteration
v_Index := v_Index + LENGTH(v_Error) + 1;

/* An error looks like 'facility-number: mesg'. Get each
piece out for insertion. The facility is the first 3
characters of the error.
*/

v_Facility := SUBSTR(v_Error, 1, 3);

-- Remove the facility and the dash (always 4 characters)
v_Error := SUBSTR(v_Error, 5);

-- Next get the error number
v_ErrNum := TO_NUMBER(SUBSTR(v_Error, 1, INSTR(v_Error,
':') - 1));

-- Remove the error number, colon & space (always 7 chars)
v_Error := SUBSTR(v_Error, 8);

-- What's left is the error message
v_ErrMsg := v_Error;

/*
Insert the errors, and grab the first error number and
message while we're at it
*/

INSERT INTO error_stacks
(module, seq_number, error_order, facility, error_number,
error_mesg)
VALUES
(p_Module, p_SeqNum, v_ErrorOrder, v_Facility, v_ErrNum,
v_ErrMsg);

IF v_ErrorOrder = 1 THEN
v_FirstErrNum := v_ErrNum;
v_FirstErrMsg := v_Facility || '-' || TO_NUMBER(v_ErrNum)
|| ': ' || v_ErrMsg;
END IF;

v_ErrorOrder := v_ErrorOrder + 1;
END LOOP;

-- Update the errors table with the message and code
UPDATE errors
SET error_number = v_FirstErrNum,
error_mesg = v_FirstErrMsg
WHERE module = p_Module
AND seq_number = v_SeqNum;

/*
Unwind the call stack to get each call out by scanning the
call stack string. Start with the index after the first call
on the stack. This will be after the first occurrence of
'name' and the newline.
*/
v_Index := INSTR(v_CallStack, 'name') + 5;

/* Loop through the string, finding each newline. A newline
ends each call on the stack.
*/
WHILE v_Index < LENGTH(v_CallStack) LOOP
-- v_End is the position of the newline
v_End := INSTR(v_CallStack, v_NewLine, v_Index);

-- The call is between the current index and the newline
v_Call := SUBSTR(v_CallStack, v_Index, v_End - v_Index);

-- Skip over the current call, for the next iteration
v_Index := v_Index + LENGTH(v_Call) + 1;

/*
Within a call, we have the object handle, then the line
number, then the object name, separated by spaces. Separate
them out for insertion.

-- Trim white space from the call first.
*/
v_Call := TRIM(v_Call);

-- First get the object handle
v_Handle := SUBSTR(v_Call, 1, INSTR(v_Call, ' '));

-- Remove the object handle,then the white space
v_Call := SUBSTR(v_Call, LENGTH(v_Handle) + 1);
v_Call := TRIM(v_Call);

-- Get the line number
v_LineNum := TO_NUMBER(SUBSTR(v_Call,1,INSTR(v_Call,' ')));

-- Remove the line number, and white space
v_Call := SUBSTR(v_Call, LENGTH(v_LineNum) + 1);
v_Call := TRIM(v_Call);

-- What is left is the object name
v_ObjectName := v_Call;

-- Insert all calls except the call for ErrorPkg
IF v_CallOrder > 1 THEN
INSERT INTO call_stacks
(module, seq_number, call_order, object_handle, line_num,
object_name)
VALUES
(p_Module, v_SeqNum, v_CallOrder, v_Handle, v_LineNum,
v_ObjectName);
END IF;

v_Callorder := v_CallOrder + 1;
END LOOP;

IF p_CommitFlag THEN
COMMIT;
END IF;
END StoreStacks;

END ErrorPkg;
/

四 游标转化
DBMS_SQL.TO_CURSOR_NUMBER
DBMS_SQL.TO_REFCURSOR
这两个函数可以进行游标转换。
declare
l_rcursor sys_refcursor;
l_colCnt number;
l_descTbl dbms_sql.desc_tab;
begin
open l_rcursor for select * from all_users;

dbms_sql.describe_columns
( c => dbms_sql.to_cursor_number(l_rcursor),
col_cnt => l_colCnt,
desc_t => l_descTbl );

for i in 1 .. l_colCnt
loop
dbms_output.put_line( l_descTbl(i).col_name );
end loop;
end;
/

declare
l_cursor number := dbms_sql.open_cursor;
l_refcursor sys_refcursor;
l_data varchar2(30);
begin
dbms_sql.parse
( l_cursor,
'select username from all_users where rownum <= 5',
dbms_sql.native );
if ( dbms_sql.execute( l_cursor ) != 0 )
then
raise program_error;
end if;
l_refcursor := dbms_sql.to_refcursor(l_cursor);
loop
fetch l_refcursor into l_data;
exit when l_refcursor%notfound;
dbms_output.put_line( l_data );
end loop;
close l_refcursor;
end;

五 本地编译
本地编译比普通编译更慢而且不能调试,但运行速度更快。
alter procedure p compile
PLSQL_Warnings = 'enable:all'
PLSQL_Optimize_Level = 2
PLSQL_CCFlags = 'Simple:true'
PLSQL_Code_Type = interpreted
reuse settings
/

alter procedure p compile
PLSQL_Warnings = 'enable:all'
PLSQL_Optimize_Level = 2
PLSQL_CCFlags = 'Simple:false'
PLSQL_Code_Type = native
reuse settings
/

六 复合触发器:
create trigger t for update on t compound trigger
g_data varchar2(200) := 'begin state';

before statement is
begin
dbms_output.put_line( g_data );
dbms_output.put_line( 'before statement...' ); g_data := 'before statement, ';
end before statement;

before each row is
begin
dbms_output.put_line( 'before each row...' ); g_data := g_data || 'before each row, ';
end before each row;

after each row is
begin
dbms_output.put_line( 'after each row...' ); g_data := g_data || 'after each row, ';
end after each row;

after statement is
begin
dbms_output.put_line( 'after statement...' ); g_data := g_data || 'after statement';
dbms_output.put_line( g_data );
end after statement;
end;

七 Named and Mixed Notation in SQL
create or replace function f
( x in number default 1,
y in number default 2,
z in number default 3 )
return number
as
begin
return x+y+z;
end;
/
pause

clear screen
select f(4,5,6) from dual;
select f(10, z=>200) from dual;
select f(z=>10) from dual;
select f(y=>20,z=>10) from dual;

八 闪回数据归档
这个功能很强,如果有足够的归档闪回数据,你甚至可以闪回十年前的数据。

使用show_space工具

这个工具也是大叔写的,用来查看表或者索引所占的空间还是很方便的,只需要登录到相应的用户下,执行脚本就可以了。用法应该不用我说了,看看脚本内容就知道了。

create or replace procedure show_space(
p_segname in varchar2,
p_owner in varchar2 default user,
p_type in varchar2 default 'TABLE')
as
l_free_blks number;

l_total_blocks number;
l_total_bytes number;
l_unused_blocks number;
l_unused_bytes number;
l_lastusedextfileid number;
l_lastusedextblockid number;
l_last_used_block number;

begin
dbms_space.free_blocks(
segment_owner => p_owner,
segment_name => p_segname,
segment_type => p_type,
freelist_group_id => 0,
free_blks => l_free_blks );

dbms_space.unused_space(
segment_owner => p_owner,
segment_name => p_segname,
segment_type => p_type,
total_blocks => l_total_blocks,
total_bytes => l_total_bytes,
unused_blocks => l_unused_blocks,
unused_bytes => l_unused_bytes,
last_used_extent_file_id => l_lastusedextfileid,
last_used_extent_block_id => l_lastusedextblockid,
last_used_block => l_last_used_block );

dbms_output.put_line( 'Free Blocks');
dbms_output.put_line( l_free_blks );
dbms_output.put_line( 'Total Blocks');
dbms_output.put_line( l_total_blocks );
dbms_output.put_line( 'Total Bytes');
dbms_output.put_line( l_total_bytes );
dbms_output.put_line( 'Unused Blocks');
dbms_output.put_line( l_unused_blocks );
dbms_output.put_line( 'Unused Bytes');
dbms_output.put_line( l_unused_bytes );
dbms_output.put_line( 'Last Used Ext FileId');
dbms_output.put_line( l_lastusedextfileid );
dbms_output.put_line( 'Last Used Ext BlockId');
dbms_output.put_line( l_lastusedextblockid );
dbms_output.put_line( 'Last Used Block');
dbms_output.put_line( l_last_used_block );
end;
/
grant execute on show_space to public;

使用TOM的run_stats工具

使用TOM大叔的run_stats工具已经好长时间了,感觉这个工具在进行数据库中几种操作方法比较的时候还是挺有用的,所以介绍一下这个工具。

一 安装:
1)权限
run_stats工具需要对V$STATNAME, V$MYSTAT, v$TIMER and V$LATCH的访问权限,因此必须以sys账户登录,赋予用户相应的权限。

grant select on sys.v_$timer to public;
grant select on v_$mystat to public;
grant select on sys.v_$statname to public;
grant select on sys.v_$latch to public;

2)安装
登录自己准备安装run_stats的用户,
运行以下语句:
create global temporary table run_stats
( runid varchar2(15),
name varchar2(80),
value int )
on commit preserve rows;

create or replace view stats
as select 'STAT...' || a.name name, b.value
from v$statname a, v$mystat b
where a.statistic# = b.statistic#
union all
select 'LATCH.' || name, gets
from v$latch
union all
select 'STAT...Elapsed Time', hsecs from v$timer;

create or replace package runstats_pkg
as
procedure rs_start;
procedure rs_middle;
procedure rs_stop( p_difference_threshold in number default 0 );
end;
/

create or replace package body runstats_pkg
as

g_start number;
g_run1 number;
g_run2 number;

procedure rs_start
is
begin
delete from run_stats;

insert into run_stats
select 'before', stats.* from stats;

g_start := dbms_utility.get_time;
end;

procedure rs_middle
is
begin
g_run1 := (dbms_utility.get_time-g_start);

insert into run_stats
select 'after 1', stats.* from stats;
g_start := dbms_utility.get_time;

end;

procedure rs_stop(p_difference_threshold in number default 0)
is
begin
g_run2 := (dbms_utility.get_time-g_start);

dbms_output.put_line
( 'Run1 ran in ' || g_run1 || ' hsecs' );
dbms_output.put_line
( 'Run2 ran in ' || g_run2 || ' hsecs' );
dbms_output.put_line
( 'run 1 ran in ' || round(g_run1/g_run2*100,2) ||
'% of the time' );
dbms_output.put_line( chr(9) );

insert into run_stats
select 'after 2', stats.* from stats;

dbms_output.put_line
( rpad( 'Name', 30 ) || lpad( 'Run1', 12 ) ||
lpad( 'Run2', 12 ) || lpad( 'Diff', 12 ) );

for x in
( select rpad( a.name, 30 ) ||
to_char( b.value-a.value, '999,999,999' ) ||
to_char( c.value-b.value, '999,999,999' ) ||
to_char( ( (c.value-b.value)-(b.value-a.value)), '999,999,999' ) data
from run_stats a, run_stats b, run_stats c
where a.name = b.name
and b.name = c.name
and a.runid = 'before'
and b.runid = 'after 1'
and c.runid = 'after 2'
-- and (c.value-a.value) > 0
and abs( (c.value-b.value) - (b.value-a.value) )
> p_difference_threshold
order by abs( (c.value-b.value)-(b.value-a.value))
) loop
dbms_output.put_line( x.data );
end loop;

dbms_output.put_line( chr(9) );
dbms_output.put_line
( 'Run1 latches total versus runs -- difference and pct' );
dbms_output.put_line
( lpad( 'Run1', 12 ) || lpad( 'Run2', 12 ) ||
lpad( 'Diff', 12 ) || lpad( 'Pct', 10 ) );

for x in
( select to_char( run1, '999,999,999' ) ||
to_char( run2, '999,999,999' ) ||
to_char( diff, '999,999,999' ) ||
to_char( round( run1/run2*100,2 ), '99,999.99' ) || '%' data
from ( select sum(b.value-a.value) run1, sum(c.value-b.value) run2,
sum( (c.value-b.value)-(b.value-a.value)) diff
from run_stats a, run_stats b, run_stats c
where a.name = b.name
and b.name = c.name
and a.runid = 'before'
and b.runid = 'after 1'
and c.runid = 'after 2'
and a.name like 'LATCH%'
)
) loop
dbms_output.put_line( x.data );
end loop;
end;

end;
/
grant execute on runstats_pkg to public;

二 使用
在第一条语句运行之前,执行
exec runStats_pkg.rs_start;
运行第一条语句,运行之后执行:
exec runStats_pkg.rs_middle;
运行第二条语句,运行之后执行:
exec runStats_pkg.rs_stop;

关于ORA_ROWSCN的一点问题

作者:江枫 | 【转载时请务必以超链接形式标明文章原始出处和作者信息及本声明】
地址:http://rdc.taobao.com/blog/dba/html/243_about_ora_rowscn.html

以下是引文:

Oracle10g引入了一个新的ORA_ROWSCN的伪列,可以查询表中记录最后变更的SCN。这个新的伪列在某些环境下会非常有用,比如执行乐观锁定,或者增量数据抽取的时候。但是,默认情况下,每行记录的ORA_ROWSCN是基于Block的,除非在建表的时候执行开启行级跟踪(create table … rowdependencies)。

先来简单理解一下ORA_ROWSCN的实现原理。我们知道,每个Block在头部是记录了该block最近事务的SCN的,所以默认情况下,只需要从block头部直接获取这个值就可以了,不需要其他任何的开销,Oracle就能做到这一点。但是这明显是不精确的,一个block中会有很多行记录,每次事务不可能影响到整个block中所有的行,所以这是一个非常不精准的估算值,同一个block的所有记录的ORA_ROWSCN都会是相同的,基本上没有多大的使用价值。

如果在建表的时候开启行级跟踪选项,Oracle则可以为每一行记录精确的SCN,那么显然不能再直接从block头部获取。要获得足够的信息,肯定要付出一定的代价,Oracle必须为每一行实际的存储这个SCN。所以这个行级跟踪的选项,只能在建表的时候指定,而不能通过alter table来修改现有的表,否则需要修改每一行记录的物理存储格式,代价是可想而知的。

简单的做个实验就可以知道开启行级跟踪以后块记录格式的不同。建两个表,一个norowdependencies(默认),一个rowdependencies,然后分别dump出相应的数据块:

create table t1(i int);
insert into t1 values(1);
insert into t1 values(2);
commit;
create table t2 rowdependencies as select * from t1;

norowdependencies:

block_row_dump:
tab 0, row 0, @0×1f9a
tl: 6 fb: –H-FL– lb: 0×1 cc: 1
col 0: [ 2] c1 02
tab 0, row 1, @0×1f94
tl: 6 fb: –H-FL– lb: 0×1 cc: 1
col 0: [ 2] c1 03
end_of_block_dump

rowdependencies:

block_row_dump:
tab 0, row 0, @0×1f7c
tl: 12 fb: –H-FL– lb: 0×0 cc: 1
dscn 0×0000.00029ae4
col 0: [ 2] c1 02
tab 0, row 1, @0×1f6d
tl: 12 fb: –H-FL– lb: 0×0 cc: 1
dscn 0×0000.00029ae4
col 0: [ 2] c1 03
end_of_block_dump

得到行的SCN后,通过SCN_TO_TIMESTAMP函数可以转化为时间:

SQL> select SCN_TO_TIMESTAMP(ora_rowscn) from t2 where rownum<2;

SCN_TO_TIMESTAMP(ORA_ROWSCN)
---------------------------------------------------------------------------
06-JAN-09 05.31.20.000000000 PM

乐观锁和ORA_ROWSCN
需要select … for update做悲观锁定的时候,通过使用ORA_ROWSCN可以改成乐观锁定。一开始select数据的时候将ORA_ROWSCN查出来,修改后如果要写回数据库之前再比对下最新的ORA_ROWSCN就可以知道这期间数据是否有发生变化。这个Tom在他的大著《Expert Oracle Database Architecture 9i and 10g Programming Techniques and Solutions 》中也是有提到的。

增量数据抽取和ORA_ROWSCN
每次抽取后记录最大的ORA_ROWSCN,下次抽取再基于上一次的SCN来获得最近修改过的数据即可。在10g之前,很多系统要实现增量数据抽取,要么通过解析日志,要么加触发器,要么就在表上加一个时间截字段。ORA_ROWSCN其实就是第三种方式,只是这个字段由Oracle来维护,这样可以避免一些应用绕过时间截去更新其他字段带来的问题。不过,如果系统中使用了逻辑备库或者streams等逻辑复制的方案,而数据抽取又是基于逻辑备库的话,ORA_ROWSCN就可能对抽取后的数据分析有影响了,因为通过这个得到的时间是逻辑备库上记录变更的时间,而不是源库的时间了。当然,如果纯粹只是做数据抽取,而不需要使用这个时间来做分析,还是问题不大的,但还是要考虑一旦逻辑备库出现故障需要重做的,则这个增量抽取要怎么来处理的问题。

Metalink上搜一下ORA_ROWSCN可以看到不少相关的bug,所以在生产系统中使用的时候要小心。例如,我在Linux64平台上的一台测试库中,执行以下语句就会碰到ORA-07445的错误:

SQL> select ora_rowscn from x$bh where rownum<2;
select ora_rowscn from x$bh where rownum<2
*
ERROR at line 1:
ORA-03113: end-of-file on communication channel

ORA-07445: exception encountered: core dump [qkafix()+212]
[SIGSEGV] [Address not mapped to object] [0x000000004] [] []

以下是自己的观点和测试:

自己觉得,其实不光在OLAP系统里有问题,在OLTP系统里也照样有问题,会增加SQL执行的次数。在系统默认的块级SCN列情况下,只要修改同一个块里的任意一条数据,就会导致其他的操作落空,而用户必需重新进行其操作,因此会大幅度的增加SQL的执行次数,特别是在热点数据块比较集中的时候,会产生很大的性能问题。而如果是行级的SCN列的情况下,就不会有这个问题存在,但是正如作者所说,“要获得足够的信息,肯定要付出一定的代价,Oracle必须为每一行实际的存储这个SCN”,说白了,行级SCN列还是一种以空间换取时间的做法。

举个例子说:
create table t1(i number) pctfree 99;
insert into t1 select rownum from all_objects where rownum<=100;
commit;
create table t2 rowdependencies as select * from t1;

fy@ORCL> select rowid,i,ora_rowscn from t1 where rownum<=20 order by i;

ROWID I ORA_ROWSCN
------------------ ---------- ----------
AAARC+AAFAAAAAZAAA 64 970530
AAARC+AAFAAAAAZAAB 65 970530
AAARC+AAFAAAAAZAAC 66 970530
AAARC+AAFAAAAAZAAD 67 970530
AAARC+AAFAAAAAZAAE 68 970530
AAARC+AAFAAAAAZAAF 69 970530
AAARC+AAFAAAAAZAAG 70 970530
AAARC+AAFAAAAAaAAA 71 970530
AAARC+AAFAAAAAaAAB 72 970530
AAARC+AAFAAAAAaAAC 73 970530
AAARC+AAFAAAAAaAAD 74 970530
AAARC+AAFAAAAAaAAE 75 970530
AAARC+AAFAAAAAaAAF 76 970530
AAARC+AAFAAAAAaAAG 77 970530
AAARC+AAFAAAAAbAAA 78 970530
AAARC+AAFAAAAAbAAB 79 970530
AAARC+AAFAAAAAbAAC 80 970530
AAARC+AAFAAAAAbAAD 81 970530
AAARC+AAFAAAAAbAAE 82 970530
AAARC+AAFAAAAAbAAF 83 970530

20 rows selected.

fy@ORCL> select rowid,i,ora_rowscn from t2 where rownum<=20 order by i;

ROWID I ORA_ROWSCN
------------------ ---------- ----------
AAARC/AAFAAAAAtAAA 64 970587
AAARC/AAFAAAAAtAAB 65 970587
AAARC/AAFAAAAAtAAC 66 970587
AAARC/AAFAAAAAtAAD 67 970587
AAARC/AAFAAAAAtAAE 68 970587
AAARC/AAFAAAAAtAAF 69 970587
AAARC/AAFAAAAAtAAG 70 970587
AAARC/AAFAAAAAtAAH 71 970587
AAARC/AAFAAAAAtAAI 72 970587
AAARC/AAFAAAAAtAAJ 73 970587
AAARC/AAFAAAAAtAAK 74 970587
AAARC/AAFAAAAAtAAL 75 970587
AAARC/AAFAAAAAtAAM 76 970587
AAARC/AAFAAAAAtAAN 77 970587
AAARC/AAFAAAAAtAAO 78 970587
AAARC/AAFAAAAAtAAP 79 970587
AAARC/AAFAAAAAtAAQ 80 970587
AAARC/AAFAAAAAtAAR 81 970587
AAARC/AAFAAAAAtAAS 82 970587
AAARC/AAFAAAAAtAAT 83 970587

20 rows selected.

假设现在有一个用户想要更新t1表的ROWID为AAARC+AAFAAAAAZAAA的数据,他运行了这条语句:
fy@ORCL> update t1 set i=10000 where rowid='AAARC+AAFAAAAAZAAA';

1 row updated.

fy@ORCL> commit;

Commit complete.

这时查询表里的数据如下:
fy@ORCL> select rowid,i,ora_rowscn from t1 where rownum<=20 order by i;

ROWID I ORA_ROWSCN
------------------ ---------- ----------
AAARC+AAFAAAAAZAAB 65 970638
AAARC+AAFAAAAAZAAC 66 970638
AAARC+AAFAAAAAZAAD 67 970638
AAARC+AAFAAAAAZAAE 68 970638
AAARC+AAFAAAAAZAAF 69 970638
AAARC+AAFAAAAAZAAG 70 970638
AAARC+AAFAAAAAaAAA 71 970530
AAARC+AAFAAAAAaAAB 72 970530
AAARC+AAFAAAAAaAAC 73 970530
AAARC+AAFAAAAAaAAD 74 970530
AAARC+AAFAAAAAaAAE 75 970530
AAARC+AAFAAAAAaAAF 76 970530
AAARC+AAFAAAAAaAAG 77 970530
AAARC+AAFAAAAAbAAA 78 970530
AAARC+AAFAAAAAbAAB 79 970530
AAARC+AAFAAAAAbAAC 80 970530
AAARC+AAFAAAAAbAAD 81 970530
AAARC+AAFAAAAAbAAE 82 970530
AAARC+AAFAAAAAbAAF 83 970530
AAARC+AAFAAAAAZAAA 10000 970638

20 rows selected.
我们可以看见块号为AAARC+AAFAAAAAZ的所有行的SCN都发生了变化,假设有另外一个用户同时也要更新ROWID为AAARC+AAFAAAAAZAAB的行,但是发起操作的时间稍微考后一点,虽然没有人修改这一行数据,但是由于第一个用户的修改操作,这时的SCN已经变化了,其操作会落空,那么该用户不得不重新进行一次操作,这显然不是我们想要的结果。

而反观行级SCN,
fy@ORCL> update t2 set i=10000 where rowid='AAARC/AAFAAAAAtAAA';

1 row updated.

fy@ORCL> commit;

Commit complete.
查询表里的数据:
fy@ORCL> select rowid,i,ora_rowscn from t2 where rownum<=20 order by i;

ROWID I ORA_ROWSCN
------------------ ---------- ----------
AAARC/AAFAAAAAtAAB 65 970587
AAARC/AAFAAAAAtAAC 66 970587
AAARC/AAFAAAAAtAAD 67 970587
AAARC/AAFAAAAAtAAE 68 970587
AAARC/AAFAAAAAtAAF 69 970587
AAARC/AAFAAAAAtAAG 70 970587
AAARC/AAFAAAAAtAAH 71 970587
AAARC/AAFAAAAAtAAI 72 970587
AAARC/AAFAAAAAtAAJ 73 970587
AAARC/AAFAAAAAtAAK 74 970587
AAARC/AAFAAAAAtAAL 75 970587
AAARC/AAFAAAAAtAAM 76 970587
AAARC/AAFAAAAAtAAN 77 970587
AAARC/AAFAAAAAtAAO 78 970587
AAARC/AAFAAAAAtAAP 79 970587
AAARC/AAFAAAAAtAAQ 80 970587
AAARC/AAFAAAAAtAAR 81 970587
AAARC/AAFAAAAAtAAS 82 970587
AAARC/AAFAAAAAtAAT 83 970587
AAARC/AAFAAAAAtAAA 10000 970673

20 rows selected.
可以看到只有最后一行的SCN列的值发生了变化,而这种情况下显然不会引起上述问题。

当然如果不想因为使用行级SCN带来存储空间的增长,我们也可以考虑将数据分布到不同的数据块上,这就需要DBA监测热点数据分布。

欢迎有问题讨论

2009年1月11日星期日

清华博士王垠的退学申请

从别人那里转载过来的,呵呵,有人说他是理想主义,也许吧。我不知道该怎么评价他。当然我也没有资格来评价别人。不用我再注释作者了吧。上次那篇完全使用gnu/linux工作也是他写的。

经过深思熟虑,我决定放弃清华大学的博士学位。其中的原因,你们有兴趣的话可以看看下面的文章。这就是我的一生与中国教育的故事。一个用血和泪换来的教训。

清华梦的粉碎—写给清华大学的退学申请 2005.9.22
清华梦的诞生
小时候,妈妈给我一个梦。她指着一个大哥哥的照片对我说,这是爸爸的学生,他考上了清华大学,他是我们中学的骄傲。长大后,你也要进入清华大学读书,为我们家争光。我不知道清华是什么样子,但是我知道爱迪生和牛顿的故事。清华,大概就是可以把我造就成他们这种人的地方吧。我幼小的脑海里就想象出我能在清华做的事情……我的脸上浮现出笑容。我说我要实现这个“清华梦”。这就是清华梦的诞生。
小小科学家
我相信每个人在小时候都跟我差不多,对这个世界充满了好奇。
鲁迅有他的百草园,我也有我自己的"实验田"。如果说小时候的鲁迅是一个艺术家,那么小时候的我就是一个科学家。这么说可能有人要说我口气太大,张口闭口就是这家那家。然而在我的字典里,"艺术家"和"科学家"并不是什么了不起的人,它们只是贴在人内心的一个标签。如果一个小孩专注于内心对世界的感觉,那么他就是一个艺术家。而我不是。我的大部分兴趣是在了解世界是怎样运转,甚至不惜代价。也许大部分男孩子都是这样。
我小时候住在父母执教的中学里。两间平房,门口有一小块地,妈妈在里面种了一些菜。我们一家三口虽然穷,但是过着宁静舒适的生活。我们在这个地方一直住到上初中的时候。这些房屋记录着一个年幼的科学家的探索和实验,直到它们被夷为平地。
妈妈拒绝让我养猫狗,她说凡是会拉屎的都不养---除了我。所以我小时候就喜欢与蚂蚁作伴。我总是试图用各种各样的办法去了解蚂蚁的生活习性。我可以一整天的观察我家屋檐下的蚂蚁来来去去。看见他们用触须碰一碰,然后各自分头走开,我就会想它们到底说了什么。我在想,能不能用一种方法解开蚂蚁语言的密码。我从书中得知蚂蚁洞里有蚁后,她有很大的肚子。为了一睹芳容,我开始试图水漫金山,把水往蚂蚁洞里灌。我有时一个下午就干这种事情,却没有一次成功看到蚁后。后来才知道蚂蚁是如此精明的下水道工程师,水大部分都渗到地底下去了。可是我不甘心,我开始试用别的办法。比如在洞口放一块糖。可是蚁后架子太大,终究不肯出来,让别人帮她送饭进去。
有人说,这个世界最后不是毁在疯子手上,就是毁在科学家手上。世界上如果只有科学家是很可怕的,比如他们会发明高效的杀人武器。我发现疏松的棉絮可以迅速的燃烧,就想出一种惨绝蚁寰的大屠杀实验。我先把糖水滴在地上,等蚂蚁把那个地方围个水泄不通的时候,铺上棉花,点火……现在想起那些勤劳的小黑头都变成灰烬,我仍然心惊肉跳。他们的灵魂会来找我报复吗?后来这个实验有一个升级的版本用的是浸泡过一种化学药品溶液的纸,文火燃烧,由于燃烧速度慢,杀伤力不大,这个实验可以测试蚂蚁的逃跑路线。我还用活蚂蚁进行过心理实验。首先用破袜子摩擦塑料尺产生静电,然后放在一只正在行走的蚂蚁身后不远处。蚂蚁走不动了,我就开始推测它在想什么,它感觉到什么。它可能会觉得有外星人?但是由于尺子拿开以后,它若无其事继续走,我猜它只是有点纳闷,而不惊慌。但是反反复复几次之后,它明显有罢工的意思,似乎忘了自己要去干什么。后来我又发现蚂蚁被吸到塑料尺上之后会由于带上相同的电荷而被"发射"出去,就像人间大炮一样。注:"人间大炮"是日本电视剧《恐龙特急克塞号》里的一种可以把人当作炮弹发射的威力很大的电磁装置。
一点微小的发现,就可以引发大量的探索和实验。这就是我在那个年代的特点。虽然妈妈也逼着我练习书法,绘画,还多次获奖,但我不喜欢这些东西。我似乎生下来就是科学家,不是搞艺术的,不过也许只是妈妈的强迫让我反感了艺术而已。物理是我最喜欢的,因为它让我了解到世界的奥秘。我一般开学前几天就会把物理书上的实验都挑出来,费尽辛苦找到材料实践一番,心里美滋滋的。上学真是快乐!
失之交臂
上了高中,由于课业的压力,我的生活逐渐改变了。为了考上清华大学,我努力的学习。抛下我的毛笔,抛下我用来做实验的蚂蚁,电池和线圈,抛下除了考试科目的一切。在老师眼里我是一个听话的好学生,在妈妈眼里我是一个听话的好孩子。每天早上按时起床,吃一大碗妈妈做的面(为了补充一上午学习需要的体力),然后冲进教室,按照预设的程序开始读书,做练习题。似乎一切都有条不紊,顺利进行。可是……
忽然有一天我发现,我的一切活动都是在纸上进行的,看书,做习题。试卷和复习书让我变得变得麻木。我想这样下去我就不再像爱迪生和牛顿了。于是我开始调皮起来。我不但要做考试的题目,还要做更难的题目。做了物理奥林匹克的题目,接着就想看大学的物理书,接着就想恢复我小时候的实验的爱好。老师辅导自习时经常被我缠住问一些不着边际的问题,那其实是我在实验中发现的问题。终于有一天,在我要求他跟我合作制造一个磁悬浮陀螺的时候,他显示出了不耐烦:“王垠,你让我先回答别的同学的问题好不好?你的问题对考试没有好处。” 我呆住了,启发我让我爱上物理的人,尽然对我说出这样的话。后来想一想,他也是无奈啊,不过我从此再也不想问他任何“超纲”的问题。
高二的时候妈妈就拿回一份前一届的高考题让我做,我随手一做就得了一个当时可以考上清华的成绩。我的心里想,清华我来了。明年的这个时候,我就会拿到录取通知书了!从此我就不再把高考放在眼里。我开始钻研越来越难的题目,进行越来越离谱的实验。我想,清华里面应该都是我这样的学生吧,我会有很多志同道合的朋友,不用再跟这群只会做题的呆子在一起了。
可是我的行为总是受到老师的压制,他们要把我们变成考试的机器。他们告诉我,沉下心来做习题,考试才能有把握。妈妈也帮着老师劝导我。看,一班的某某某这次模拟考试数学成绩比你高,多努力一下吧。我哪里听得进去,我才不在乎这点分数,我能解决更难的问题,老师都没法解决的问题。我开始有了逆反心理,开始早上懒床,装病请假不去上课。班主任,校长多次找我谈话,说我要沉下心来准备考试云云。但是我根本就听不进去,我鄙视高考,觉得他们没有资格出题来考我。然后我就有了心理疾病,大概是强迫症。高考语文的时候我居然怀疑监考老师认为自己在作弊,接着好像真的怕被抓住了一样,手发抖,头冒汗。然后我又想要是考不好,以前的优秀会不会也被人怀疑?他们会不会以为我以前的成绩全都是作弊得来的?手就抖得更厉害了。这时候,监考老师可能发现了我的情况,真的走了过来,站在我身后。害得我好几分钟不敢写一个字,因为手已经完全不听使唤。不过他还是走开了,这可怕的高考终于结束了。
我们是考试前填的志愿,我根本不考虑其他学校就只填了清华。后来妈妈研究了一下,帮我添了一个天津大学在第二志愿。以下的志愿全部空白。大家觉得我真够大胆,可是我的心理状态让我发挥完全失常,比清华的最低分数线还差两分。特别是语文,才 96分。天津大学第一志愿收满不要我。昔日的好学生,居然到了落榜的下场。我真的那么好吗?我问自己。我太骄傲,才落到如此地步吧。我开始怀疑自己是否应该那样瞧不起高考。看着爸爸的愁眉苦脸,妈妈的唠唠叨叨,真是生不如死。复读吗?那会是噩梦的继续。我不能再在这个学校待下去。再面对题海,我的心理疾病会让我自杀的。碰巧四川大学来招收高分落榜的学生,还给了我随便选择专业的机会。妈妈说,计算机现在很火热,出来好找工作。我虽然对工作不感兴趣,但是我比较喜欢写程序,于是就进了川大计算机系。
两度退学失败
不能不说进川大是个没有选择中的好选择。大学生活自由一些,我至少不会走上自杀的道路。可是我的毛病仍然在继续,我永远不满足学校里能学到的那么点东西。老师基本是照本宣科,我逐渐不再满足这种知识灌输式的教育。我觉得完全没必要上这个大学。
川大的环境我实在无法忍受。军训的时候受够了同学和教官的委屈,我就想退学。我们的军训是在一个戒备森严的炮兵基地里,心里的苦向谁说啊!有一天我们正在路上齐步走的时候,我忽然看到一个女人挽着一个军官走了过来。那个军官的老婆怎么长的这么像我妈妈!要是妈妈来到我身边该多好!没想到回到营地,团长(原来是连长,我们来军训他就升一级做团长了)说有人来探访。我走过去,居然发现是妈妈!因为听说我想退学,她急忙向学校打听了军训的地点,几经周折跑过来,是那个军官带着她混进来的。我想我妈妈要是转行当间谍一定是个好料子。她说已经帮我办了退学,学校同意了,回去好好复习,准备考上清华…… “好好复习,好好复习”……我的脑海里又浮现出高三的情景,这次我要跟一群更没用的复读的人在一起。脑子一阵疼痛之后,我说:“妈妈,我不想退学了。”
可是军训回到学校,发现宿舍如此差劲,我又想退学。妈妈又来帮我办理手续,可是结果我还是由于懦弱反悔了。害得学校办事的老师都骂我:“你这个人简直神经病!” 对啊,我确实是有病,不过我的是精神病,不是神经病。我恨我的高中,我恨我的大学,我恨高考,我恨中国的教育!是你们让我生病的。可是妈妈,她为了我已经费尽了辛苦。我不能再这样周折下去。我自己在学校里好好努力,准备考上清华的研究生吧。
学校住宿环境很差劲,又经过好多麻烦事,我终于决定在校外去租房子住。后来我开始玩滑板,它让我变得勇敢。我心里逐渐平静下来,可以用心看书了。大二以后,我的学习生活才逐渐进入正常,自信开始恢复。
梦的复苏
记得川大教Pascal语言的老师第一堂课就对我们说:“我们学校就是落后啊。外面公司 里都用C, C++了,我们还在教Pascal。你们以后要出去工作恐怕还是得学学VC什么的。” 于是有的同学开始抱起一本本像“XXX圣经”之类的书开始学习,上数学课也在看这些东西。我当时自愧不如啊。自己就是小学的时候玩过一下学习机,可以说没有任何计算机基础。辅导员也经常夸他们几个动手能力强,以后公司就需要这样的人。他们出口就是Bill Gates, 世界首富云云。军训的时候听着他们说什么DOS, 温95,我就只有张着嘴崇拜的份了。才想起我高中计算机竞赛的时候一道有关DOS命令的题没有做出来,现在听他们说才知道原来DOS是个“操作系统"。那操作系统又是什么,他们说每个电脑上都必须有一个操作系统……我真是愧不如人 -_-!
正在我决定鼓起勇气后来跟上,准备拿起一本DOS大全从头啃起的时候,一次偶然的机会我 接触到了Linux。后来又因为The Art of Computer Programming,接触到了Knuth。我才发现,课堂上讲的那些东西原来如此低级,有些东西学了就过时,学它干吗?我并不比别人落后多少。我一再的思考,什么是计算机科学?是什么让我们计算机系的人不同于其他系的。我有时候认为有了答案,但是后来答案又被我自己推翻。在思想的混乱中,我发现我逐渐摆脱了旁人的标准。我不再想像别人那样去考计算机等级考试,对微软的认证也不屑一顾。我自己学会了Linux,还会很多种当时别人听都没听说过的计算机语言。我学会了LaTeX,还因为找出Knuth书里的错误得到两张支票和一些礼物。这并不是什么值得炫耀的,但是这给我对计算机的兴趣很大鼓舞,我从此更加认真的看书。上课要不就逃掉,背地里拿着大部头的“龙书”之类的原版英语书啃。要不就看我打印出来的GNU的一些资料,完全不听老师讲。期末划重点的时候也不去,考试却总能考个八九十分。总有几个女生排名在我上面,不过我不在乎这点分数,考试和分数不再能评价我。同学们大概都觉得我是一个怪人,后来毕业了我才听他们说,他们管我叫“怪才”。我如此努力的学习着,对别的事情充耳不闻。我只有一个目的,就是毕业就离开这个鬼地方,进入清华大学上研究生。虽然大家不理解我在干什么,清华的老师应该挺在乎我学的东西吧。
可是我没有想到,在我死啃书本的时候,我的创造力正在离我远去。在我盲目接受我认为高深的材料的时候,我失去了自己的创造。我成了比别人稍微好一点的技术工人,不再跟爱迪生和牛顿是一类人了。我高中的时候拼命想保存的创造力已经在苦读之下消失殆尽。我看书的方式变得顺序化,总想从头看到尾。我的高中老师们的目的,中国教育的目的,终于快达到了。
清华,我来了
大三的暑假,我来到清华想拿一些考研的资料。这是我梦中的地方呀,美丽的校园,比川大要大上好多倍吧,脚都走痛了才走到招待所。去系办,一个办事员态度很不好的给我一份资料。哎,学校好,人脾气就该大啊。忍了吧,要是真能考进来就好了。
后来听一个老师说清华有一种学生叫“直博”,可以硕博连读,五年拿到博士学位。只要面试通过就可以进来学习。我心想这种方式好啊,我平生最讨厌的就是考试了。出高考题的那帮人,他们有什么资格考我!考研资料也是遍地飞。写了几本复习材料就自称什么“一代名师”,我最看不起这种人了,就会赚钱。我如果可以获得“直博”的名额,就可以永远摆脱他们了。想一想,要是硕士三年,博士三年,就要六年。现在五年就可以拿到博士学位,还不用考试,真是太好了。可是我又有什么资格获得清华的直博?我在川大从来没听说过这种东西。
于是我就开始打电话联系老师,跟他们谈谈。面对他们的眉头,面对他们的笑脸却无可奈何的说“没有名额”,我都感觉没什么希望了。一个院士甚至对我说:“你们四川大学是什么学校?二流都算不上,最多算个三流大学。你怎么能来我这里!” 我深受打击,可是我还是没有放弃。最后我找到了一个老师,我们一开始就谈的挺投机。他听说我跟Knuth有过联系,挺高兴的说,哦我知道他,好多年前来我们这里做过报告呢。我终于觉得找到了知音,于是决定就跟着他学习。老师找好了之后还有一个面试,是别的老师参加的,我说什么他们似乎没有认真听,就一个劲看我的考试成绩这种我不屑一顾的东西。我面试时特意穿上了Knuth送我的MMIX T-shirt,他们大概根本不知道是什么,我也没有提起。
不过老师只对我的体育成绩提出了疑问,说你怎么才80多分?你的身体能不能胜任繁重的学习任务啊?我笑着回答,我每天还跑5000米呢,我们学校打分比较严,难道清华的学生体育都考90?面试就这样通过了。
推荐信与散伙饭
面试通过后回到学校还要办一些手续。成绩单,推荐信等等,跟申请外国大学研究生院差不多,让我感觉挺正规的。院长对我挺好的,同意帮我签推荐信。可是签完字之后他对我说:“你别以为他们觉得你是个人才。他们是根本招不到人!他们那里像你这样的学生都出国了,剩下的是最差的。谁想读博士啊?你别太高兴了。” 我笑着应付这突如其来的打击,在心里却不断为自己的选择辩护。清华一定是好样的,不会让我失望。它是我的梦啊。
很多麻烦的手续之后,终于拿到了我梦想的大学的录取通知书,可以离开川大这个鬼地方了。毕业的散伙饭上,看着大家喝得酩酊大醉,还有人在咆哮说居然连川大的文凭都没拿到,我一个人默默地想象着即将到来的清华的快乐生活,暗自庆幸。
散伙饭到了尾声的时候,我诧异的看到一个平时不太熟悉的同学拿着一杯啤酒走过来。我挺紧张,我最不喜欢别人给我敬酒了,说是客气,其实很虚伪。没想到他说:“我敬你一杯,大牛人。听说你被清华大学录取作了博士。我干了,你随意。” 我不知如何回答,我一向不知如何应付别人的恭维。还好他没有让我也干杯,倒是够尊重人。没想到喝完他接着说:“我知道你是怎样的人。我很仰慕你,你是真正喜欢研究的人。可是我要告诉你,清华的人并不会比我们好多少。大部分人也只是想混一个学位,将来找个好工作。没有多少人可以跟你一起研究的,你去了必定很孤独。我就很奇怪你这样的人怎么不出国呢!你会后悔的。”
我有点不高兴了。一个人说你的选择是错误的,你的反应是什么呢?反正我当时为我的“清华梦”作了一番辩护,说我进去自己好好研究,应该还是能够很好的,毕竟这是我从小的梦啊。可是没想到,他说的居然是对的,我现在开始感谢他了。
计算几何,创造力的复苏
清华还是一样的上课方式,大部分课也是很多人一起上,一起打瞌睡。老师也是照本宣科,我居然发现他们其实跟川大的老师没什么区别。清华的不同之处就是,一到考试的时候原来进行的一切娱乐活动都不见了人影。原本每天晚上都有人一起玩轮滑,考试的时候就只剩下我孤零零的一个人。因为大家都怕考试,开始熬夜复习了。还有就是上课不容易逃课了,有些老师会突然点名,缺席会严重影响最后的成绩。
对于博士生,传说还有一个规定,那就是后10%淘汰。也就是说,不管你成绩如何,如果成绩排名在课程的后10%,那么就要重修。而如果两门功课重修,就会被开除。面对如此残酷的规定,很多同学都惶惶不可终日。我就是在隔壁同学的唠叨声中度过了第一期。不过我还是没有把考试当回事,所以我也没有去验证这个说法的官方真实性。我仍然不去听老师划重点,我仍然不觉得老师出的题目有什么好,我仍然讨厌有人让我们用手算矩阵。可能觉得太残酷,还是觉得要是开除了博士生谁来干活,这条规定后来改成了如果博士生上了80分就可以不重修。我也不知道为什么我觉得考砸的科目也上的了80,故意放我过去的吗?
但是我的生命中出现了这样一门课程。它改变了我对老师的看法,让我觉得上课原来也可以如此有趣。这就是计算几何。上课的人很少,只有十来个人。因为听说这门课很难,很多同学都没有选。但是我就是那种知难而进的人。老师上课的方式跟别的课程很不一样,大家坐在一个小教室里,老师有精美的幻灯片,有动画,不时还插入一段大科学家,大哲学家的名言。上课时老师会停下来很多次让学生提问题,下课大家都积极踊跃的讨论新奇的问题。课程的评分方法也很特别,平时成绩占到30%的分量,作业分为几种分值,可以自己选择做不做,作业的总分数乘以30%,加上最后大作业的分数乘以70%,就是最后的得分。说真的,这门课太有趣了,我就只逃过一次课。但是还是有时候人数不到一半,因为其他课程压力太大,有人都去复习别的课程了。但是邓老师从来不点名,还对逃课的同学表示同情。还问我们在座的有没有其他课特别紧张的,下次课可以不来。真是让人感动。
我就是在这门课上认识了王益,我们亲密无间的合作,让我领略到了什么叫做研究。大作业的时候我们在一个小组,其实是三人一组,但是那第三个人其实什么也没干。我和王益决定写一个3D的Voronoi图扫描算法演示程序。王益的3D图形编程能力很强,所以他做界面,由我负责算法生成数据作为后端。我们分别在自己的机器上编写程序,不时的打电话讨论接口的设计问题。我找到了Bell labs 的 Steven Fortune 的算法程序,决定看懂它,然后改造成演示需要的分部运行的算法。但是 Fortune 的程序几乎没有注释,而且使用了一种奇怪的数据结构,很难理解。Fortune 还在程序里说到,这个算法虽然有效,但是对于程序员来说是一个挑战。所以我email请他给我一份算法论文的拷贝,他同意了。但是一个月之后,信才到我手里,那时我们已经完成了作业。因为我花了一个星期看懂了他的程序,还换掉了他的麻烦又低效的数据结构。随后成功的把后端与王益的前端设计好接口联合。等我看到 Fortune 的论文,发现程序里面其实已经改进了论文的核心内容。其中的parabolic transformation其实完全没有必要实现。我深深体会到实践的重要性,也许先有了他的论文我反而会被误导,写不出实际可以运行的程序。
由于我们的团结努力,老师对我们的大作业非常满意,他给了我们最高的分数 100。由于我们两个都在课下超额完成作业,所以总的分数我们两个都是满分。这是我阔别已久的100分。只有在小学我才拿到过这种分数啊!对于一个对考试成绩满不在乎的人,100 又意味着什么?如果是别的课程我会毫不在乎,就像我得了80分一样。可是这个100分是我们团结研究而来的,它包含了对我们的合作意识,对我们的友谊,对我们的热情的肯定。虽然我觉得我们的东西还有改进的余地,但是我接受这个100分!也只有这样的课程,我才可能得100分。
从此我感觉到了什么叫做研究。这跟我小时候干的那些事情没有什么两样。你在身边发现一个问题,想知道为什么。然后你就想去获得解决这个问题的知识。你去看书,你去问专家,你上网去搜索。如果没有发现答案,那么好啦,你就可以自己试图去发现为什么,这是最有趣的部分。知道了为什么,就想让这个东西有用处,对人们的生活产生好处。这就是研究。
《完全用Linux工作》与TeX的推广
这么说来我还是对清华有些好感。遇到一个好老师让我从呆头呆脑的技术工人的状态恢复过来,开始追求自己的梦想。可是第一年把所有的课程上完之后,我就发现原来清华所谓的“研究”是如此混沌。其实清华大部分人进行的所谓的“研究”是什么呢?其实就是写作,不是科学研究。这一点以后我会详细叙述。
远远看去外观华丽的有着先进的工作站的实验室,却没有可以安心看书的地方。机器挨着机器人挨着人,书都没地方放。师兄师姐们都在忙着用word写论文,不时有两个人隔着几行机器大声谈话。实验室通风不好,还有一个大型工作站在嗡嗡作响,我进去一会儿就觉得头晕,所以后来就不想去了。PC机以前都是公用的,每次都会用不同的机器,却没有我想用的软件,麻烦死了。好不容易实验室买了新机器分配给个人,装上一个 Linux 系统开始写程序,还在Sun工作站上安装了多达1G的GNU程序。却被一个师兄嘲笑说那种跟DOS一样落后的东西你居然也用。于是我写了一篇文章叫做《完全用Linux工作》,放在主页上驳斥这种观点。矫枉过正,确实写的优点偏激,结果引起网上linux界轩然大波。后来我又发现几乎全校的论文都是 word排版的,那些公式质量太差,看起来头痛,才发现很多学生害怕数学的原因之一。所以又写了文章宣传 TeX,希望中国产生更多漂亮的数学书。这下子我出名了,真没想到,出名不是因为我的研究成果,而是因为这些业余的东西。我起初不希望我因此出名,但是看到旁边的人都用上了 TeX,我觉得我还是做了一件好事,至少让论文看起来漂亮了一些。
可是论文的内容,却是我永远的痛!
培养计划
我在第一年就把功课全部上完了。本来我想多选几门课,比如法语,可是清华的博士要选课需要提交一个“培养计划”给导师签字。导师同意之后才能修改。导师看到我选了法语,就说这个第二外语还是自己学学就行了吧,旁听也行啊,我主要是怕你课太多了考试不通过就麻烦了。我当时没有说什么,就把法语去掉了,只留下刚够学分的课程。其实我还想选很多的,体育,音乐什么的,都不好意思跟导师说。后来才知道宿舍对门的硕士生选了钢琴课他们导师都不管。为什么我们就受到如此待遇?
可是没有把法语加到培养计划却成了我的遗憾。有一个新学期我去旁听了第一节法语课之后老师就说,我知道很多同学是来旁听的,这样教室里人太多了,效果不好。这对自己对大家都不好,下次请旁听的同学不要来了。我脸皮薄,下次就没有去了。后来自己想自学却又没有老师教,看了十集reflet之后就此作罢。
后来我终于明白了,清华不需要全面发展的博士生,而其实导师还会在某种程度上削弱学生的能力。导师并不是真的为我们好,而是不喜欢我们上课,因为上课不但会花掉研究(或者干活)的时间,而且让他们眼界太开阔,这样学生会很容易有别的选择而走掉。所有的活动:助教,实习,都必须有导师签字。而大部分导师就会找借口不让学生干这些事情。不给他们助教和实习的机会,让他们以后不好找工作,只能为自己服务,或者为自己的熟人服务。甚至这次我去西藏,要办边境证都要有导师签字。办事的老师说,没有导师签字,你跑出去了不回来怎么办?大妈,我跑那种地方干吗?
除了这些,还有两大法宝就是博士学位和违约金。清华的博士学位有多值钱知道吗?不知道?那么博士退学要交几万块钱的违约金,这下大部分穷苦学生怕了吧。这就是你们的卖身契。清华就是这样把研究生牢牢地控制在自己的掌握之下。我对一个如此害怕学生跑掉的不自信的学校还能说些什么?如果你是好样的,就不会害怕我们跑掉!该跑掉的最后终究会跑掉。
我的自我培养
在学习上,我永远是个吃不饱的人。选不了课,我就去旁听。旁听后觉得老师讲的不好,我就自学。在我有空的时候,我就会去图书馆借书看。在我本科的时候,我就已经发现自己的一个特点,我会很快发现新的东西,并且学会使用它。虽然这些东西并不是创新,但是它们丰富了我的技能,让我有更大的能力去进行创新。我经常顺藤摸瓜似的从一个问题搜索出一大串我想知道的东西。然后借一大堆书回来,每本看一点点,只为找到我需要的答案。
计算几何课的一次作业,我为了写一个算法的演示程序,花了3天时间学了一点Java语 言,正好能够完成那个程序。我开始接触到TeX的底层细节,看完了The TeXbook,并且找出一道练习题答案的错误。开始移植gbkfonts程序,作为我的CWEB语言的练习。看完了几乎所有 Xlib 的手册,了解了 XWindow 的工作原理。我接触到 Scheme,并且做完了 SICP 的大部分习题,还自己想出好多问题用Scheme实现算法。后来花了好几个晚上,把MIT课程6.001的录像下载回来。我才发现教授上课可以如此搞笑有趣,上课时戴上巫师的帽子,做一些滑稽的表演。我终于明白,有的计算机科学家居然可以去好莱坞演电影 :) 这个课程让我领会到 LISP 的强大,改变了多年以来对这种古老语言的误解。它让我感觉到在看似纷繁复杂,不断更新的计算机语言的世界,还有那么一种永恒的美!接着我又学会了 Common LISP,并且开始用它来设计研究计算几何的一个函数库。另外还找了一些希奇古怪的程序来玩,写了一些心得体会放在网上给别人看。
我意识到自己数学还不够强,甚至有些怕,就开始看一些数学方面的书。Concrete Mathemtatics, What is Mathematics?, Science and Hypothesis, Godel Escher Bach, ... 虽然每一本都没有看完,但是我逐渐相信自己的数学能力,发现数学原来如此有趣,并不是做习题那么枯燥,也不像一辈子就拼命证明一个定理那么清高。才发现国内很多数学书用难看的符号把学生吓倒了,其实想通了就是很直观的原理。
我看了电影 A Beautiful Mind 之后深受感动,就去买了一本原著的书,它是数学天才John Nash的传记。它描写了20世纪初的Princeton,一群科学家生活的情景。我眼前浮现出在一个房间里,一群人在喝茶聊天下棋讨论问题激烈争论。我发现我从小内心向往的,就是那样的地方。我看到Nash是如何用“头脑暴力”解决一个他没有任何基础知识的问题。原来只要有了问题和探索的精神,就会有动力去获得解决它所需要的知识,最后将问题解决。发现有用的,重要的问题,而不只是寻找困难的问题,这样才会对人类有价值,才会有动力。我还看到一个真正的数学天才是怎样的喜欢恶作剧,又怎样因为过度的傲慢狂妄,想向世人证明自己的天才而发疯。我发现世界上有远比科学更宝贵的东西。我开始悔悟我高中时对待成绩不好的同学的态度。我不是一个天才,但是我要做一个好人。
但是我的研究却没有多少进展,至少我自己这么认为。我发现问题的根源,就是没有真正的讨论,没有真正的问题。
我们也有讨论,原来是这个样子
上完课,就该开始搞研究啦。可是研究什么呢?老师给我几篇论文看,意思是让我看看有没有什么想法。
我开始感觉没有头绪,就跟导师说能不能找师兄师姐跟我讨论讨论,还有别的人在做这个吗?他说,就你一个人做这个,每个人做一个题目,独立思考,这就是研究。我觉得是啊,我应该独立思考。可是过了一段时间发现不行啊,我想实现一个想法,但是我不知道是不是已经有人试过失败了。实验的时间开销会比较多,所以我想知道那么多厉害的人,为什么都不用这种明摆在那的方法?当我再次提出需要讨论的时候,他似乎有点生气的说:”你为什么总是想有人跟你做一样的东西啊?你不是想抄袭别人的论文吧?” 我不发话了。继续做我的实验,结果确实不理想。虽然自己实践很重要,可是要是能利用别人的经验,何乐而不为呢?这并不是偷懒。如果有人讨论,很多时候一个人提出一个问题,另外的人可能就会告诉他这个问题是不是有人做过,有什么重要性,凭直觉告诉他有什么难度。可是如果没有讨论,连问问“有没有人做过”的机会都没有!
后来我就经常上网看看国外的大学怎么搞研究,发现他们都有 seminar,讨论组。A Beautiful Mind 描述的 Princeton 以前的天才们每天都在一个地方喝茶,讨论问题,争得面红耳赤,回家分头思考,做实验,第二天喝茶时再讨论。那就是我从小梦寐以求的生活啊!计算几何课已经让我爱上了与人合作和讨论的方式,现在却孤零零一个人了。我必须告诉导师,合作和讨论是非常重要的。在我据理陈述之后,他说:“好吧。反正师兄师姐各自有自己的事,你要讨论什么就跟我和你副导师讨论吧。” 于是我就开始了跟他们两个星期一次的见面讨论。每次讨论都感觉他们不知道我在说什么,他们心里想的都只是这个能比别人的好多少呢?能不能投到这个会议呢?如此宏观。我觉得跟他们讨论完全是浪费时间。
后来课题逐渐有了新的同学加入,导师决定跟中科院数学所的人一起申请一个项目来研究。于是我们每两个星期去中科院讨论。不过感觉他们那边也差不多。中科院的老师觉得他们的研究太理论,期望我们能给他们带去一点实际的东西。可是我们也没有什么实际的东西,所有的问题都是从别人的paper里看到的。副导师就开始跟他们说这个问题有多么多么重要…… 他们也借此机会开始研究以前放下的一些问题。总之讨论的感觉就是没有目的,没有主题。有时有人说他在想一个什么问题,说了一会儿就被否决了。有时候就是一个人看了一篇paper之后做一个感想。我坐在那里就在想,我们到底在干什么?我们甚至都不知道什么东西值得研究,还研究什么?后来师弟师妹们就开始考虑把问题变一变,看看能不能产生新的问题。他们的做法,我跟他们开玩笑说就是“有问题也要解决;没有问题,制造问题也要解决!” 他们笑着点点头,“本来就是这样嘛。没办法啊。”
博士生论坛的时候,同学们都觉得有类似的问题,讨论不足,交流不足。所以我提议成立一个 类似国外大学的 Common Room,用来讨论问题。可是大部分老师说:“这样一个房间,天天都要有那么多人在里面待着。谁来出这个钱?” 是啊,老师自己的办公室都要钱,哪里可能有什么 Common Room?就算有了 Common Room,在里面讨论的无非还是文章发到哪里的问题。制度决定了行为,我的设想太理想化了。
分析一下,为什么老师不提倡讨论呢?因为问题是有限的。老师辛辛苦苦这么多年搞来搞去都在搞这些问题,分配给你们每人一个,互不冲突。要是两个人都搞一个问题,这下好了。出了成果论文归谁?学校要求必须第一作者才算论文数。要是两个人都写论文,那么投到同一个会议肯定有一个要被reject。这样对集体发展不利嘛,大家不就是发几篇论文混毕业吗?何苦?
paper, paper, 还是paper
说到paper我就痛心。我的方向上我至今还没有看到几篇我觉得像样的文章。我主要进行集成电路布线算法的研究。看起来高深,其实是很简单的问题,一个平面上有一些点是电路里的电极,现在需要用铜线把它们连起来,怎么样让连线的长度或者时延最短?这个问题跟几何上一个有名的问题 Steiner tree 问题有关系。我的导师就是以前写了一篇这样的paper发到IEEE transactions。
已经毕业的一个师兄就在他研究的基础上修改来修改去,发了好几篇paper。英文的不够还翻译成中文,投到国内的期刊。后来一个师姐又在这个师兄的基础上进行修改,又发了好多篇。可是在我看来,他们的论文纯粹就是炒冷饭,没有什么创新。一个问题解决了,那么解决问题的人显示了他们的聪明,至于这个问题对人有什么用,他可以暂时不管(虽然我也严重反对这种做法)。后来又有人来搞这个问题,多半是被老师分配来的。他也小修改一下,修改想法其实不费工夫,主要是你怎样把你的 Introduction 写好?可以让别人觉得你的工作有意义?这就是功夫。作家的功夫。我有一次面见INRIA的头目 Jean-Claude Paul 时,他就对我说:“Tsinghua students are all writers, not scientists.”
现在清华研究生做的事情无非就是,写好paper,然后找个地方投出去。SCI 的最好,EI的其次。偏僻的没人看的杂志也没关系,交钱也没关系。我就知道日本的一个SCI索引的期刊收1000美元的版面费。导师出钱,不投白不投,投了好毕业呵!
现在我也被“分配”来做这个问题。虽然说是一个有名的问题,但是这个有名的问题已经被研究了好几十年了。有很多挺厉害的人做出了很重要的贡献,但是我们为什么研究这个问题?我至今没有搞懂。
开头导师只是给了我两篇paper,据说是以前他一个得意门生写的,美国某大学的副教 授。其中有一篇说是如何在不构造 Delaunay triangulation的情况下生成 MST (最小生成树)。看到这篇文章开头说在 rectilinear metric下, Delaunay triangulation 就不能用来构造 MST 了,所以他设计了一个新的算法。这个算法比起 Leo Guibas 的算法更加简单。文章里还提到一次 Matroid,让初出茅庐的我觉得高深莫测。我还专门去借了一本《Matroid Theory》来看,其实他的论文剩下的部分跟Matroid没有任何关系。可是我对“Delaunay triangulation 不能用来构造 RMST” 这个说法产生了怀疑。经过理论分析我觉得即使在 rectilinear metric下,Delaunay triangulation 也可以用来构造 MST 的。我觉得作者只是故意这么写,想为他设计算法的动机找一个借口。我决定实践我的想法,写一个程序从Delaunay triangulation 构造出一个 RMST。这本身不是什么创新的工作,可是我却在想,这样一个东西能不能用来构造 Steiner tree 呢?后来我真的就想出一个办法。实验表明我的算法比以前的算法要快几倍。
这是不是说我的算法是一个值得写paper的东西呢?导师说我应该写一篇,但是我认为我只是在挑别人的毛病时意外想出了一个改进的算法,并不会对将来的研究有什么启发。虽然程序快了一些,但是很少有那么大的线网需要这么快的算法,而且几倍的提高在我眼里不算是一个理论上的改进,而且这个算法不能推广到其他距离空间,可扩展性很低。所以我内心觉得这个结果不令我兴奋,不想写论文。但是在老师的一再要求下,我居然把这个研究写成了两篇paper。按照他的说法:“应该分阶段总结你的成果。” 起初投出去的时候评委总是说这个东西不实用,导师说这是评委的问题,他们觉得不实用我们就投到理论一点的会议。经过几次投稿,还是失败了。我终于忍不住了,对副导师说出我的想法,我说:“看一个作家的水平,是看他扔在垃圾筐里的纸。就让我把这篇paper永远藏在我的垃圾筐里吧。” 但是他不甘心,说你要相信自己的实力,然后把我的算法胡乱夸奖了一番。我说我不管了,随便你怎么办。我就开始研究我自己喜欢的东西去了。之后他居然真的投中一个欧洲的会议,是被 LNCS 收录的,LNCS 是 SCI 索引的,所以我居然有了一篇 SCI 文章!我自己不喜欢的文章也是 SCI 了!
第二篇论文就更传奇了。几投不中,就其原因,评委说是没有和现在“最先进”的算法程序实验比较。而我没有比较的程序,就是那个让我觉得发paper动机不纯的人的程序。没办法,求他给我代码。比了一下,确实比他快。不过我估计他程序写的有毛病,老是 core dump。而且从实验数据来看,运行时间增长的速度不符合他论文里声称的时间复杂度。但是没办法,他只给 binary,也不给源代码。程序快几倍,很有可能是实现上的问题,而不是算法更好。我的一个师兄以前就把他自己的算法戏称为“基于bug的优化”。我觉得这样比较对那个算法的作者不公平,完全没有发表的价值了。但是没办法,谁叫我们都是出来混的,没有人在乎这些。我还是记录下数据,添到论文上。一投就中,得了一个最佳论文奖。然后就有一篇校内新闻宣传:“我校王垠同学获得XXX会议最佳论文奖。这是大陆学者首次在如此高级别的会议上获得如此高的奖项。 ” 这个“高级别”的会议,在我看来就是个垃圾。美国人都把最差的论文投到这里,就是为了来旅游一圈而已。
我对自己的做法产生了深深的负罪感,觉得自己正在进入这团混沌,正在被同化。我决定换一 个题目研究。我就开始考虑zero skew tree. 找了20多篇paper来看,发现他们没有什么本质的改进。而且对于问题本身的价值,他们完全就不清楚。有的作者后来甚至说,其实以前他们考虑的问题是没有必要解决的,因为实际应用中不可能遇到,我们其实可以把问题变成这样……本来一句话就可以说清楚的事情,又写成了好几篇paper。我就是这样在 paper的海洋中,找不到目标。
我见过的这种低级别的会议,低质量的论文几乎都是从 IEEE 那里出来的。道理很简单,IEEE 会议多,会议论文集都像两大块砖头,还是双列小字排版,当然能容纳下这么多的垃圾了。所以我对 IEEE 也没有好感。
火山小规模爆发
第一篇投中了会议之后,副导师很高兴的说“代替我去开会”,到希腊Santorini岛玩了一圈。回来还跟我说希腊不好玩,好苦啊,幸好你没去。然后就继续要我为算法申请一个专利。
写这个论文我都已经焦头烂额了,一点都不感兴趣。现在还要写专利,“要像教小学生做这件事一样,一步一步的把算法写清楚,举出实例”。我觉得快不行了,再这样折腾下去,我到博士毕业也许也就只搞出这些小儿科东西吧!我终于小规模爆发了一次。我坦荡的告诉了副导师我的想法,我觉得做学问应该是什么样,我觉得这么点东西不值得申请专利。我还告诉他我对国内的研究环境很失望。
他慌了,可能以为我想要退学,赶忙找我谈谈。对我说,我知道你心中有很大抱负。所以这次就不写专利了。我知道你想有更好的研究环境,但是不踏踏实实做好现在的工作,又怎么能有大的创造呢?然后就开始举爱因斯坦,居里夫人的例子…… 然后说,其实你在这里好好努力,将来出国的机会多的是,你想去Harvard也行,你想去Princeton,都行啊!
你说行就行?你去去给我看看?我们实验室从来就没有去这些地方的。继续这样做下去,以后哪个真正的科学家还会要我?
全面发展
在对清华的研究完全失望了之后。我就准备考GRE,TOEFL出国了。我去上了一个新东方的班,没学到什么英语方面的东西,倒是接触了很多新的思想。老罗的言论特别有趣,虽然我不是完全赞同他的意见。写GRE作文特别培养思维能力。我为了写 GRE作文,常常为了一个不明白的问题到图书馆翻阅英文的哲学书籍,有关教育的书籍…… 对于很多问题我得到了完全不同的观点。大学的目的是什么?人的价值观是由理性决定的吗?等等等等。我读到了亚里士多德,柏拉图,康德等人的言论。甚至有个哲学家说 "All Animals Are Equal". 我看了他的文章觉得有很多可以批驳的观点。我看到迪卡尔的文章,说“要掌握科学就要掌握它的全部”,这句话真合我心意,我就是想做一个懂很多东西的人啊。我想结合艺术与科学。虽然我这个观点得到一些人的批判,但是我仍然相信迪卡尔。
从这些互相矛盾的观点中,我有了自己的判断力。我开始能够揭开从小蒙在我眼睛上的有色眼镜看问题。我开始检查我自己的思维,我以前的观点。看看它们是否是未经判断就盲目放进去的。我检查到很多很多的错误。我的待人接物,我对他人的理解上,都有不足之处。我还检查到妈妈传递给我的一些有色眼镜,小学课本给我们的有色眼镜。我开始学会用自己新的方式对待他人,看待事物。我不再盲目相信权威,哪怕他是诺贝尔奖得主,图灵奖得主。我有了自己的自由思维。
在那段时间,我感觉我的心智大门被开启了。我开始尝试从来没有做过的事情,以及从来不认为我能做好的事情。我一次又一次的相信我能。我能学会画画,我能打好太极拳,我能理解古典音乐…… 世界还有那么多美好的事情等着我去学习去开发啊!
可是,我们却像囚犯一样被判了5年在清华。博士学位就是我们的枷锁。
醒悟,paper的奥秘
清华研究生谈论的重点是什么?是 paper。吃饭的时候谈,喝茶的时候谈,睡觉的时候也谈。隔壁的同学在进校第一年就为paper惶惶不可终日,说:“你知道吗,他们要求我们发SCI,怎么办呢?我几个师兄都是因为没有paper延期毕业的。” 这恰好就是那个为后10%淘汰惶惶不可终日的同学。他的老师是个院士,可是他在手下就干一些写word文档之类的杂活还忙得要命,根本没有时间思考问题。
后来听说学校有规定,博士生必须发4篇paper才能毕业,其中必须有一篇是SCI索 引,或者两篇EI索引。看上去冠冕堂皇的SCI, EI,不就是跟 google 差不多的东西吗?被它索引了怎么样了?特别是对文章的篇数作要求,而对质量没有判断。我其实读了两年都还不知道学校是这样规定毕业标准的。当我知道的时候,已经有人告诉我SCI=Silly Chinese Index。真是让人啼笑皆非。
学校没有能力评价学生的水平,就拿文章数来衡量。这样的毕业标准造就的是怎样的学生,怎样的实验室呢?难道导师真的没有能力判断paper的好坏吗?有些是,但是有些不是。即使他知道你的论文没什么价值,也会叫你发表。我发现paper数量的背后,是某些人的如意算盘。想一想是怎么回事吧。国家看什么来拨款研究?看paper。看什么来评价一个学校的水平,也是paper。国家没有能力评价你的能力,当然只有看你有多少paper。所以有了paper就有了钱。只要你能写paper,培不培养你,你将来的发展,关我们什么相干?你写的 paper别人能不能看懂,能不能转化成生产力,管我们什么相干?怪不得有的院士想尽办法也要多收学生,宁愿自己帮学生出学费也要他进来。因为学生就是财源。paper可以带来基金,可以在美国买小车洋房,没有基金就让学生干活吧。一个月几百块钱吊着一条命在那里为你拼命,谁叫他们想要那个博士学位呢!
该清醒了,博士无产阶级!
告别清华的博士学位
现在我已经厌烦了国内所谓的“学术”。我准备放弃清华的博士学位,出国找个好老师,进行真正的研究。博士第4年了,做出这样的决定真是不容易。有人告诉我不要放弃,你知道有多少人正在羡慕你?你知道一个清华的博士学位有多么值钱吗?但是我不能这么沉默下去了!
博士学位,累坏了多少年轻的中国人!我不再为它浪费我的青春。我知道国外大部分研究也不是那么好,如果国外也找不到好的老师,我就永远离开学术界,找一个简单的工作,和我心爱的人一起生活。有人说这是浪费人才?在清华混沌的过日子才是浪费呢!当一个侍者至少也让我感到对社会有贡献,看着顾客满意,我会露出笑容。可是做一个博士却没有。我感觉自己是个没用的人。
我已经完全看透了中国教育的失败。我高中的时候就受到它的伤害,这种伤害延续到现在。中国教育已经成为埋没人才的祸首。留在这个圈子里就是屈服,我不出声,大家都不出声,这个世界就会继续这样郁闷的运转下去。我今天要对这个系统大声地说一声“不!”
我离开了。可是中国永远也不缺少为清华拼命的人!因为他们的妈妈会告诉他们,清华是全中国最好的学校。你要考上清华,为我们光宗耀祖……
行动
2005 年9月22日下午3点,我在东主楼导师的办公室里跟导师和副导师再次重申了我的想法。包括以上的一切,和我准备退学,准备10月份考托福的打算。导师经过一番举例爱因斯坦,居里夫人,叫我踏踏实实的说教无效之后,严厉的批评了我只顾自己,不顾及教研组为我付出的心血。然后说:“要是你不能再为实验室作研究,我们就不能支持你了,前两个月实验室发的钱我收回。你可以马上写退学申请,我们实验室没有什么损失,我们有的是人干你的事情。不过我要告诉你,你一旦退学,连学校的住宿都要被收回!”
接着是副导师尖声的咆哮:“是啊,你瞧不起我们。我们是没有你聪明,可是我们勤勤恳恳……你知道你得的那个best paper award,我们付出了多少努力吗?你认为这么容易拿到吗?那是多少国外专家鉴定……”
我安静的等他说完。真像是一场闹剧,一场梦。他平息下来之后,我说了一声“再见”,然后默默地走出了办公室。
你们要退学申请?这里就是我的退学申请。
尾声
晚上收到副导师的email说:“还有一件事需要向你说一下:你在学校学习期间所取得的成绩包含你的努力、导师的指导帮助、同学们的帮助,还有学校和国家的支持。你作为博士生学习阶段取得的成果属于教研组、学校和国家。正如同我们作为职务发明的专利属于学校一样。
你在MST、SMT等方面取得了结果,它属于教研组、学校和国家。单位有责任进行合理的应用,为国家建设、国家荣誉服务。有责任进行进一步的整理丰富、向高水平的刊物投送。这里我们想说明一下上述的情况,同时,也告诉你一下:你若愿意将这些成果进行进一步的整理、我们已经给你提出了具体的修改意见,欢迎你按照进行修改。你若放弃,我们将进行具体的改进、投递。我们将尊重你的意见。谢谢。”
最后还是没有忘了paper的剩余价值。进一步验证了我的判断,他们在乎我吗?不。他们只在乎paper。至于我流离失所,又有何相干?我不知道有多少无知的弟弟妹妹又会把他们的研究建立在我不屑一顾的paper之上。
后记:我写本文的目的
Repair what you can — but when you must fail, fail noisily and as soon as possible. —Basics of The UNIX Philosophy
修复你能修好的—但是如果你必须失败,那就尽快喧闹的退出。—UNIX基本哲学
我不是一个中国教育操作系统下优良的程序。我在系统里运行了将近20年,快到最后的时候 才喧闹的退出,Dump出这么大一个core file。我知道有的程序很早就退出了,我自愧不如他们。但是有的程序一声不响就退出了,还有很多的程序成为了zombie,永远的驻留在系统中成了系统的负担,在这一点上我又比他们好一些。至少我让程序员有机会用调试器检查core文件,调查这个程序运行中哪里出了问题。
“你退学就退学,干吗大惊小怪,牢骚满腹的?” 如果只是有牢骚,我就把隔壁同学拉过来一起发发牢骚就完事了。可是我虽然不是优秀的程序,我觉得应该为修复这个系统,修复自己做点什么。我希望国家的教育和研究环境好起来,这样大家就安心的生活,不用出国搞得奔波流离。有多少恋人由于一个人出国了而痛苦的分手,有多少父母在盼望海外游子的归来?我不能像很多人那样申请了国外的学校,拍拍屁股就走人。我一年前就考GRE想出国,可是我总是自欺欺人的幻想国内的境况会好起来,有时我觉得看到希望,可是马上希望又破灭了。一个个大师来了,让我一次次燃起希望,可是发现他们对环境的作用也不大。一些大师不满意,又走了。我自己也想尽力改造环境,结果经过多次努力无效,自认能力不够,终于放弃了。
在发现大家都忙着发表paper而没有讨论时,我曾经建议设立一个清华的THU- Technical Report。我的想法是:最差的草稿扔在垃圾堆里;可能有用但是还不值得向所有人公开的东西发到THU-TR,供系内查阅;如果发现THU-TR的东西会有用,再好好修改了转投会议或者期刊。系学术助理王磊很高兴的采纳了我的建议,并且自愿维护一个THU-TR的编号。可是根本没有人愿意把自己好不容易写出来的但是确实又不值得发表的东西投到这里,因为世界上总有地方可以把这个东西投出去,还是SCI和EI,而这个THU-TR连正式刊物都不算。后来有人告诉我,如果学生都把东西投到我们这里,不知道有多少导师会跟我们急。所以THU-TR的计划就这么告罢。
我一年前写信给 Knuth,这个我相信是真正的大师。我说我想退学,想请他推荐一些真正的研究者给我做老师。他回信说“你先找精通中国文化的长者谈谈”。我意识到他可能觉得这是一个文化的问题。我于是想知道中国的科技为什么搞不好,就开始看一些有关文化的东西。后来居然跑到中国社会科学院去听新竹清华大学人文学院的院长讲座,后来又在清华参加了人文学院的研讨会。会上一个老师说的好,当一个制度没法衡量学术水平本身,它就会用一个似乎等价的标准,比如paper数或者高考分数。但是一旦这个标准被确立,人们就会向着这个标准努力,而不是向学术水平本身。他们总会发现制度的很多问题,找出破绽,去达到这个标准,而不是提高自己的学术水平。最后,这个标准已经完全不能反映水平本身。我就在想,这个问题大了,这不仅是环境,制度,而且还是长久以来的文化造成的。从新竹清华大学院长的讲座里,我发现英国人是怎样用科学技术打开了中国的大门,而乾隆皇帝是如何对科学不感兴趣。中国似乎从古到今就不重视科学技术的,中国有自己的优势,自己的文化。对啊,科学技术是个双刃剑,如果照美国那样发展下去也不知道会怎么样。我们中国的文化是瑰宝,但是它已经被外国的坚船大炮打得遍体鳞伤。这不是我们的错,但是我们要努力恢复自己的文化,不能总是怨天尤人。我就开始看道德经之类的东西,还去西麓学社参加古代文化讨论活动,后来又开始打太极拳。
我觉得再没有从实际出发的目标,我的研究就会完全变成纸张了,就像我高中感觉到的一样。所以后来我就自己设立了一个研究方向,我把自己称为“研究博士生”,我要去了解博士生都是怎么样生活的。我就想知道有多少学生有跟我类似的困境。我跟很多朋友谈过,去了解他们的苦衷,研究生也有,本科的也有。我觉得我还应该了解更多的人,就试图到研究生通讯社做记者,心想挂一个记者证,就好跟人套磁问一些问题了。结果他们说我口才不好,所以做了一个秘书。后来记者们告诉我,他们是由上级分配任务的,根本不可能让你去报道学生真正的想法。我为了多多接触外国文化,比较中西文化的不同,又加入了学生对外交流协会 (ASIC),我在ASIC有了很多好朋友。博士生论坛的时候也有很多同学跟我反映研究上的问题。讨论成立特别兴趣小组(SIGs)的时候,我就提议成立一个Common Room,一个同学说她去 Stanford 的时候那里就有很好的 Common Room,很多人在一起讨论,这是国外大学斯通见惯的东西。我告诉Oxford的朋友我的想法,他很惊奇地说:“你们居然没有 Common Room?” 后来吃饭时我又找一些老师谈话,发现他们也对这个事情无可奈何。老师自己的办公室都要自己出钱,谁还能支持你们有这么大一个房间?而且即使有了房间,谁来讨论?还不就是拿着别人的paper,试图找点可以改进的地方,或者就讨论哪个会议好发paper。Common Room只是一个形式,只要有人感兴趣,随便找个茶馆也能讨论。问题就在于没有人有精力有心情进行真正的讨论,制度决定一切。我们无能为力。我觉得自己一个学生力量太小,曾经试图找大师帮忙。我找到Andy Yao,述说我的苦衷。结果他对我说:“别试图去改造环境!你没有这个能力,连我都没有!改造好你自己就不错了。" 改造好我自己,可是怎么改?所以我决定先换一个环境,到一个真正搞研究的地方去体会,去学习。
其实我不后悔进入川大,不后悔来到清华,珍惜一切的历史,因为没有它们,我也许就不是现在的我,有着自己想法的我。我也许就在安逸的生活中变得堕落。它们不完美甚至给我痛苦,但是我还是珍惜,珍惜这里的朋友,这里的一草一木。也许这就叫做爱。我会变得更好,我会挂念我的满目苍夷的祖国母亲。我会回来告诉你我学到的一切,我会给你和其他儿女真正的幸福,一定的!

2009年1月7日星期三

ORACLE内核示意图



从eygle那里转过来的,今天第一次看到这个图,原来ORACLE内核结构是这样子的,以前一直以为操作系统依赖层应该是独立于内核之外的,今天算是开眼了。

2009年1月6日星期二

中国计算机技术只落后美国两年?

北京商报近日采访了中科院计算所所长李国杰院士。李国杰指出,中国的计算机软硬件技术只落后美国两年,龙芯,汉芯,与Intel Core i7只是一代CPU的差距。并且指出,中国计算机业在六七年内做到了美国几十年才走完的道路,中国委托意法半导体开发的的龙芯和Linux操作系统的发展速度是Intel公司与微软公司的三倍。并指出国外公司对于信息安全领域仍处于探索阶段。广大中国网友闻此消息信心倍增,纷纷表示微软和英特尔公司退出中国影响不大。并极力催促中国开发完美兼容Windows无版权并免费同时能让微软公司退出中国的超级ReactOS操作系统,以及通过龙芯让英特尔公司的Core i7产品彻底失败。

这个老东西在胡扯什么呀,我们的计算机业至少落后美国20年。

在本机搭建Tomcat集群环境

1 下载tomcat和apache


2 Tomcat集群

将tomcat解压两份:

/home/fanng/Public/Server/apache-tomcat-6.0.14-A

/home/fanng/Public/Server/apache-tomcat-6.0.14-B


(1) 打开“apache-tomcat-6.0.14-A”文件夹下“ conf ”文件夹下的“ server.xml ”文件以及“apache-tomcat-6.0.14-B”文件夹下“ conf ”文件夹下的“ server.xml ”文件
a

(2) 找到 server.xml 配置文件中的“ Server ”配置项目,并进行修改。

<server port="8005" shutdown="SHUTDOWN">

<server port="10005" shutdown="SHUTDOWN">

<server port="20005" shutdown="SHUTDOWN">

说明:第一行为两个 Tomcat 修改前的情况,第二行为 A Tomcat 修改后的情况,第三行为 B Tomcat 修改后的情况。


(3) 找到 server.xml 配置文件中的相应“ Connector ”配置项目,并进行修改。

* 修改前内容如下:

<!-- Define an AJP 1.3 Connector on port 8009 -->

<connector port="8009" protocol="AJP/1.3" redirectport="8443">

A 中修改后内容如下:

<!-- Define an AJP 1.3 Connector on port 8009 -->

<connector port=" 10009 " protocol="AJP/1.3" redirectport=" 10043 ">

B 中修改后内容如下:

<!-- Define an AJP 1.3 Connector on port 8009 -->

<connector port=" 20009 " protocol="AJP/1.3" redirectport=" 10043 ">

提示:此步骤目的是修改 AJP Connector 端口。


(4)找到 server.xml 配置文件中的另一个相应“ Connector ”配置项目,并进行修改。

* 修改前内容如下:

<connector port="8080" protocol="HTTP/1.1" connectiontimeout="20000" redirectport="8443">

A 中修改后内容如下:

<connector port=" 10001 " protocol="HTTP/1.1" connectiontimeout="20000" redirectport=" 10043 ">

B 中修改后内容如下:

<connector port=" 20001 " protocol="HTTP/1.1" connectiontimeout="20000" redirectport=" 20043 ">

提示:此步骤目的是修改 HTTP Connector 端口,其中的“ 10001 ”与“ 20001 ”是未来通过浏览器访问集群中各个 Tomcat 实例的 HTTP 端口。


(5) 通过修改 Engine 配置选项,配置集群中每个 Tomcat 实例的名称。

* 修改前内容如下:

<!-- You should set jvmRoute to support load-balancing via AJP ie : <engine name = "Standalone" defaulthost = "localhost" jvmroute = "jvm1"><br />-->

<engine name="Catalina" defaulthost="localhost">

A 中修改后内容如下:

<engine name="Standalone" defaulthost="localhost" jvmroute=" Tomcat1 ">

<!-- You should set jvmRoute to support load-balancing via AJP ie : <engine name = "Catalina" defaulthost = "localhost"><br />-->

B 中修改后内容如下:

<engine name="Standalone" defaulthost="localhost" jvmroute=" Tomcat2 ">

<!-- You should set jvmRoute to support load-balancing via AJP ie : <engine name = "Catalina" defaulthost = "localhost"><br />-->


(6) 修改配置文件中的 Cluster 配置项目,对集群的各项参数进行设置。

* 修改前内容如下:

<cluster classname="org.apache.catalina.ha.tcp.SimpleTcpCluster">

A 中修改后内容如下:

<cluster classname="org.apache.catalina.ha.tcp.SimpleTcpCluster" channelsendoptions="8">

<manager classname="org.apache.catalina.ha.session.DeltaManager" expiresessionsonshutdown="false" notifylistenersonreplication="true">

<channel classname="org.apache.catalina.tribes.group.GroupChannel">

<membership classname="org.apache.catalina.tribes.membership.McastService" address="228.0.0.4" port="45564" frequency="500" droptime="3000">

<receiver classname="org.apache.catalina.tribes.transport.nio.NioReceiver" address="auto" port="4000" autobind="100" selectortimeout="5000" maxthreads="6">

<sender classname="org.apache.catalina.tribes.transport.ReplicationTransmitter">

<transport classname="org.apache.catalina.tribes.transport.nio.PooledParallelSender">

</transport>

<interceptor classname="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector">

<interceptor classname="org.apache.catalina.tribes.group.interceptors.MessageDispatch15Interceptor">

</interceptor>

<valve classname="org.apache.catalina.ha.tcp.ReplicationValve" filter="">

<valve classname="org.apache.catalina.ha.session.JvmRouteBinderValve">

<deployer classname="org.apache.catalina.ha.deploy.FarmWarDeployer" tempdir="/tmp/war-temp/" deploydir="/tmp/war-deploy/" watchdir="/tmp/war-listen/" watchenabled="false">

<clusterlistener classname="org.apache.catalina.ha.session.JvmRouteSessionIDBinderListener">

<clusterlistener classname="org.apache.catalina.ha.session.ClusterSessionListener">

</clusterlistener>

B 中修改后内容如下:

<cluster classname="org.apache.catalina.ha.tcp.SimpleTcpCluster" channelsendoptions="8">

<manager classname="org.apache.catalina.ha.session.DeltaManager" expiresessionsonshutdown="false" notifylistenersonreplication="true">

<channel classname="org.apache.catalina.tribes.group.GroupChannel">

<membership classname="org.apache.catalina.tribes.membership.McastService" address="228.0.0.4" port="45564" frequency="500" droptime="3000">

<receiver classname="org.apache.catalina.tribes.transport.nio.NioReceiver" address="auto" port="4000" autobind="100" selectortimeout="5000" maxthreads="6">

<sender classname="org.apache.catalina.tribes.transport.ReplicationTransmitter">

<transport classname="org.apache.catalina.tribes.transport.nio.PooledParallelSender">

</transport>

<interceptor classname="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector">

<interceptor classname="org.apache.catalina.tribes.group.interceptors.MessageDispatch15Interceptor">

</interceptor>

<valve classname="org.apache.catalina.ha.tcp.ReplicationValve" filter="">

<valve classname="org.apache.catalina.ha.session.JvmRouteBinderValve">

<deployer classname="org.apache.catalina.ha.deploy.FarmWarDeployer" tempdir="/tmp/war-temp/" deploydir="/tmp/war-deploy/" watchdir="/tmp/war-listen/" watchenabled="false">

<clusterlistener classname="org.apache.catalina.ha.session.JvmRouteSessionIDBinderListener">

<clusterlistener classname="org.apache.catalina.ha.session.ClusterSessionListener">

</clusterlistener>


提示:上述配置内容主要是对集群中各个 Tomcat 实例间进行通信的方式、端口以及 Session 共享算法的设置。本教程由于篇幅所限,不能一一详细介绍,有兴趣的读者可以参看 Tomcat 的官方文档,其中有非常详细的说明。


(7)分别启动两个tomcat,再分别在webapp/root目录下新建一个Hello.jsp

A out.print("Hello! This is A!!");

B out.print("Hello! This is B!!");


(8)打开浏览器分别输入:

http://localhost:10001/Hello.jsp

http://localhost:20001/Hello.jsp


3 使用apache做负载均衡

将下载的apache解压,进入解压后的目录。

./configure --enable-so --enable-mods-shared="proxy proxy_http proxy_ftp proxy_connect headers"

make

make install


然后编辑/usr/local/apache2/conf/httpd.conf

将loadmodule ssl_module ....

前的注释去掉


然后将以下加到文件末尾:

ProxyRequests Off


ProxyPass / balancer://myCluster/


<proxy>


BalancerMember ajp://localhost:10009 route=Tomcat1


BalancerMember ajp://localhost:20009 route=Tomcat2


</proxy>


./usr/local/apache2/bin/apachectl start


在浏览器里输入:

http://localhost/Hello.jsp

多刷新几遍,就可以看见A页面和B页面轮换出现。


至此Tomcat集群搭建成功。

推荐一个unix/linux体验站点

如题:
网址:
http://www.unix-center.net
sun为每人提供了100M的存储空间,还有多种unix/linux操作系统
还可以使用MYSQL数据库
在windows平台下可以使用一个SSH模拟器来登录,在linux/unix平台下使用SSH登录即可

学习中心里有很多不错的资源
特别是针对solaris和mysql的资源,相当不错

2009年1月5日星期一

起步的台阶

by 梦想风暴

原文请看这里

我的程序人生是从微软的技术起步的。虽然那时已经是Windows的年代,但是目光的局限,让我依然还是在DOS上下了一些功夫。也是因为从DOS出发,后来顺理成章的进入了Windows开发的行列。那时候的我是很努力的,不断的探索着各种各样的技术实现,不断的阅读着各种各样的书刊杂志,也着实记住了一些所谓的技术。不过,有个问题一直困扰着我,我觉得自己记住的只是一些形,而非神,这些形的东西是很容易忘记的,所以,我一直觉得自己并没有真正的理解编程,我甚至一度怀疑适不适合做开发。

真正让我开始觉得心里踏实是以程序员为职业之后。我的职业之路起步于Java,做的是服务器端的开发。跨平台的Java让我的目光不在局限于微软的平台,而服务器端的开发,让我有机会更加关注软件设计本身,而并非花哨的表现形式。随着开发越做越多,我逐渐开始摸索到了一些共性的东西,对自己的程序人生充满了信心。

走不同的路,得到的结果差异会很大。从上面提到的我个人的经历反映出在不同的技术社区内的不同倾向。记得有人说过,微软社区更倾向于探究底层实现,而Java社区更关注设计架构。其实,差别并不只这些,比如,微软的技术社区倾向于追踪新技术,因为几乎差不多每隔几年,微软就要把自己的东西推翻了重来,从DOS到Windows,再到.NET的变化,而Java社区的人则是在一个稳定的基础上不断的发展。微软是为了商业上的发展,所以,它要不断推陈出新,而Java也有变化,比如EJB到without EJB,但决定因素多半是技术上的,而非商业上的。

如我前面所说,微软社区有很多人关注的是一些细节上的实现,所以,造成的结果是他们不得不在茂密的技术丛林中不断摸索,而无暇顾及其它。我们公司内部,有一个技能列表,上面记录着哪些人会哪些东西,比如Java、.NET、Ruby等等,这样方便做项目时进行资源调配。其中有一个有趣的现象,只拥有一项技能的开发人员大多都属于.NET阵营。而Java阵营的人,经过最初的探索,会发现原来自己学到的东西是可以用在很多其它的地方,于是他们的触角开始伸向其它地方,比如JavaEye这个以Java命名的网站上会有很多关于Ruby的讨论、关于函数式编程的讨论、关于Erlang的讨论,这些东西少有会对Java开发本身产生直接的帮助。

如果已经从最初的阶段突围而出,或许,这些差异并不重要,因为我们做事靠的是自己的方法和一些共性的东西。但对于刚开始成为程序员的人来说,并不知道这些差别的存在,一个人的见识有限时,他会认为自己见到的就是整个的世界,就像我刚开始编程那会儿。

其实,我觉得最适合编程起步的应该是Unix开发。这些年里,很多出自贝尔实验室的书,比如《Unix编程环境》、《程序设计实践》、《C程序设计语言》等等都是在探索软件开发本质的书,而Unix的诞生地正是贝尔实验室。Eric Raymond写过一本《Unix编程艺术》,品味之下,便不难发现,许多随着Unix而生的是做事的方法,而这些方法并不是把人限制在Unix这个具体的平台之上,而更多的是一种通用的软件开发理念,拥有了这些理念,即便进入了一个陌生的领域,只要稍加学习,从前的感觉便会回到身边。(说得多好阿)

起步固然重要,不过,即便起步并非一帆风顺,如果抱有一个开放的心和一个思考的大脑,最终,都会走上同样的开发之路。

方法论、方法论——程序员的阿喀琉斯之踵

by 刘未鹏(pongba)
原文请看这里

以前,我认为一个事物对我没有直接用途的时候就不会去理会它,心理学上说我们都戴着自己的认知偏见的有色眼镜去有选择性地看待这个世界,纷繁的信息经过我们的认知图式过滤之后便成为少量有序的事件,所以我们都在有强烈选择性地关注一些事物和忽视另一些事物,然而,这样可能会导致丧失一些很有价值的信息,而总是将知识面停留在自己的小世界中——当然这倒也不是说看到什么都要凑上去学一学。如何在这两者中间取得折中,我觉得一个好的办法是先简略地想一下这是个什么东东,他的本质是什么,出现是为了满足什么需求,等等比较“高层”的问题(即“What”和“Why”而不是“How”),这些问题应该是可以通过简单的调研和思考得出结论的,至于背后的技术细节,如果你打算入行,就可以去学,如果不打算的话则可以免了,至少前面的思考和简单的调研能够一定程度上保证当有价值的信息或机会摆在你面前的时候你不会把眼睛蒙上走开,并且多做做这类思考对于思维的广度也很有价值。最近我开始认为,最佳的学习方法就是先广度优先遍历(先弄清 What和Why),然后择最合适的分支深入(How)(算法牛人DD同学在TopLang上的一个帖子里面也提到类似的想法,刚进大学就能够如此清晰地看清前方道路的走法,我对DD很佩服)。

方法论看似是个很抽象的东西,并且的确有一些方法论是抽象到 over-generalized (泛化过度)的地步,然而说实话在实践当中我总是发现(正确的)方法论是再现实不过的东西,比如一个大家都明白的道理是:如果方向走错了,那么做的功就基本全白费了(还有比如“如果方法对头,就能事半功倍,反之可能多走很多弯路”)——然而现实中有多少人能够真正实践这个方法呢?绝大多数人都是只顾解决眼前问题,抓了这头丢了那头,更多人是不知道问题是什么,只管把头脑中能联想到的一个以前类似情况下的类似方案套用上来。以前我总是觉得一个公司里面,CEO/CTO 这样的角色是基本摆设,但我现在不这样想了。在 How 层面把事情做好,做成一个精钻的程序员,那顶多就是能把钳子使好,这样的事情很多人都能做到,熟能生巧嘛。换句话说程序员基本上是去解决一个定义好的问题,去实施一个定义好的方案。然而决策问题就不一样了,决策问题是需要去定义问题是什么,以及权衡最佳方案是什么,不管是决策技术架构还是决策商业策略,都是非常复杂的思维过程,需要综合和权衡大量的信息,这种能力就不是简单楞着头搞下去能练出来的了,很多时候需要抬起头来看,免得只见树木不见森林。(以上也是为什么我在讨论组里面一篇帖子(什么是算法?为什么学习算法?以及学到什么程度?)中提到我觉得学数学学到精通未必就会思考日常决策问题的原因—— 数学几乎总是去解决一个定义好的问题,用的也都是定义好的严密的逻辑推导。然而现实中的问题是一个复杂系统,诸多变量互相影响,如何权衡最佳方案实际上是一个复杂的统筹规划。更重要的是,你往往甚至都不知道问题是什么,能够从纷繁的信息中抽象出问题,是一种极大的能力。这里推荐《你的灯亮着吗?》和《失败的逻辑》)

当然,我自己还没能到这个层面,尚需要不断实践和总结,所以只能稍微的谈一点感受,再往下扯只怕就会流于空泛了。这一点上我还是举一个程序员们喜闻乐见的例子吧,在程序员眼睛里面,做一个项目,也许首先想到的是用什么语言,什么框架,什么库,在这个方向上那就是什么看上去牛B用什么,恨不能都用 haskell、lisp 来写才爽,用 Java?那多没意思啊,Java 那坨弱智语法我小学的弟弟都能掌握,也没啥牛B的语言特性,忒没成就感(只可惜真正判别弱智与否的并非用什么语言技术,而是做出什么产品满足什么需求)。这就是属于只考虑单个孤立因素的简单(或者说 Naive 的)决策,这个因素就是——只要让我自己感觉爽——只可惜并不是让自己感觉爽的做法就是真正解决问题的做法,始终要弄清问题是什么,在后者意义上,一些对于技术型程序员往往没有吸引力的话题其实有着极其重大的价值——比如什么时候设计,什么时候重构,什么时候集成,再往上一层其实这些又都是次级问题,首要的问题还是这个产品满足什么需求,有什么市场(即这件事情值不值得做),有一句话想必很多人常听说,如果不知道要做什么,套上十二层架构也无济于事,方法永远不是因,而是果(我在以前的另一篇文章“Failing to see the Big Picture – Mistakes we make when learning programming”中也阐述了类似的观点)。

再举个例子,如果我想给我的网站做一个 feature ,我认为这个 feature 技术上很牛很强大,而且刚好有机会使用一下我最近修炼的某某 framework 和某某语言,而且这玩意很有挑战性,还不是一般人能够做得了的,综合以上三点,我立时觉得心痒难耐摩拳擦掌。然而实际上这个问题应该怎样分析呢?首先,考虑到以上三点,这将会是一个投入相当大的项目,那么其收益就必须要对得起这个投入,技术上很牛不代表商业上就牛,再牛再难做的 feature 如果不能带来商业价值那就是负收益。总而言之,

1. 一件事情仅仅让你感觉挺牛不代表这件事情就是值得做的;
2. 一件事情仅仅让你感到很有兴趣并不代表这件事情就是值得做的。

这两句话和我们日常的认识并不冲突,其实我们几乎总可以找到既有价值、又有趣、又有足够挑战性的工作。举个例子,本科数学学得精纯无比的同学有没有偶尔也会觉得盲目呢?做这些题目到底有什么实际用途呢?这就像是你总是在磨一把刀,磨得闪闪发光锋利无比,你可以向别人炫耀自己的刀很牛B,但是刀是为了冲锋陷阵血溅五步的,你也不想让它折戟沉沙吧,不管是将数学用在数学物理上还是用在人工智能、机器学习、密码学、通信上,都是既让人有成就感,同时又有意义和价值的事情。对我们程序员来说,你把一门语言玩得很精通,不仅知晓它所有的语法细节,陷阱和缺陷,还了解它的底层实现模型是如何。你觉得很牛很有成就感——的确,我们都会为一件自己做到了别人做不到的事情而感到自豪,然而反问一句,除了情绪价值之外,这样的事情在本身的价值上有没有你感觉到的那么牛呢?如果你只是在削铅笔,那么何必磨一把倚天屠龙剑来?反之,如果你做的是一个本身功能很牛很创新很有价值的软件,那么语言技术其实完全是次要的,并不是看上去越眩越好,关键是选择各个方面综合考虑起来最合适的工具即可,瑞士军刀也许很丑,但对于丛林冒险很实用就行。拿着一把屠龙宝刀去野外生存,同样也不靠谱。

编程语言是为了实现软件的,软件是为了服务于人的。诚然,了解一门语言的方方面面能够使你更有效地使用它,然而另一方面,如果结果发现这门语言并不何时来解决你手头的问题呢?受到投入的沉没成本的影响你一定觉得很不甘心吧?同理,由于你对一门工具投入了很多的精力,这门工具已经和你的情感挂上了钩,于是如果让你来决策用什么工具来完成一个任务的时候,你几乎会毫无疑问地想到使用你最熟悉而喜爱的那个,这就是“当你手头拿的是一个锤子,任何东西看上去都像钉子”这句话的一个解释。原先的问题是使用一门技术使得能够性价比最高地实现要完成地产品,然而到你那里问题就悄悄地变成了“使用一门技术让我感觉最爽”,问题的所有其他需要综合考虑的因素都被选择性忽略掉了,所以如果你是一个语言技术 fans ,那么几乎毫无疑问你会成为一个糟糕的决策者。最近,在学习和研究的方法论上我已经听到不止一个人(参见《失败的逻辑》和这里——一位诺贝尔奖得主的忠告(引言如下))忠告我们,不要为了工具而工具,忘记了工具的目的是什么。

…我们前面提过的显微摄影专家,擅长于拍矽藻的照片,动物学家着迷于昆虫贝壳跟有美丽羽毛的鸟类。我们的爱书狂最高兴的事情,就是读最新书和专论,他认为这些东西很重要,而且很能够刺激头脑,可是别人没办法,找到另外一本同样的书。我们博学的模范,经过这种策略,让他的朋友惊奇,让他的朋友佩服…

…没有效率的科学家中,有一小类就是所谓的崇拜研究仪器的狂人,他们对金属的闪烁非常着迷,就像是夜鹰对它在镜里的反影着迷一样。他们非常仔细照顾所崇拜的东西,把仪器拭擦得雪亮,可以当镜子使用,而且把它摆饰在最崇高的地方,就好像在教堂里面的圣坛…

CodingHorror 的作者最近在博客里面跟着 Steve Yegge 同学宣称,如果有一件事情是他想教给程序员同学们的,那就是 Marketing 。无独有偶,有一次吃饭的时候鲍志云同学也提到: Marketing Sense 是很重要的。其实也就是不要总想着写牛代码,用牛语言技术,不要落入为技术而技术的怪圈,而是首先想明白做的事情有什么价值,先弄清做什么,为什么做,再去想怎么做,这样后面的功夫才花的有价值。

You won't-- you cannot-- become a better programmer through sheer force of programming alone. You can only complement and enhance your existing programming skills by branching out. Learn about your users. Learn about the industry. Learn about your business.

当然,最后必须声明的是,不要矫枉过正,误会以上的观点,以上观点并不是说学生时代做的基本功是不需要的,一些非常基本的功夫(如计算机体系结构、数据结构和算法、两个主要流派(命令式和声明式)的编程语言都使用了哪些主要的编程范式、对主流语言的优缺点和适用场合的了解等等)是有必要掌握牢靠的,因为一方面我们并不是都能去做判断与决策,码农总是一个可靠的职业。另一方面对技术大方向的把握也是决策的基础知识,只是不要掉进无尽的技术漩涡,成为技术追星族。此外也许还有一个小小的好处就是如果被老板逼着用一门不熟悉的语言时不会很不痛快,因为真正重要的事情是你完成的产品,而不是用的语言。



一个有趣的附录

我们的思维有很多很多的弱点,前文讲的其实就是这样的弱点,更多更系统一点总结可以看下这里,和这里。我一向认为,正确的思维方式,是一切高效学习的基础。比如参见如下2个例子,错误的思维方式得到的结论有大得多的可能性是谬误。

[1] 人总喜欢沿袭以往习得的经验,并通过类比来进行外推。我第一次在一个地铁终点站坐地铁的时候,看着从远方开来的地铁,我心生疑惑——“这车每节车厢都这么长,待会怎么调头呢(我心说没看到铁轨终点有一个大大的供调头的U形弯啊)?”,当车开始开的时候我终于意识到原来车是可以往两头方向开的。

[2] 人喜欢从关联当中寻找因果,有一次我我老婆去银行取款,到了 ATM 室的自动门口,我开玩笑地拿着手头的饭卡去刷了一下,然后——门居然开了。我顿时来了劲,立即得出一个结论:这个刷卡装置不安全,至少不是能够专门识别银联的卡的。我甚至飞快地泛化出了一个更具一般性的理论来解释这个现象:即可能所有带有磁性的卡都可以用来开门。老婆看我得意洋洋,就泼过来一盘冷水:不一定是你的卡刷开的啊,你不刷卡试试看。我不信,说怎么可能呢,心想我刷卡,门就开了,还有比这更明显的因果关系嘛。但出乎我意料的是,我走出门,这次没刷卡,门也开了——原来是感应门——原先这个 ATM 室的确是刷卡门,但后来改成了感应门,刷卡的那个装置只不过没拆掉残留在那里而已。

[3] 《失败的逻辑》里面从认知心理学的角度系统介绍了我们在复杂情况下的判断与决策是如何出错的,非常值得一读。

你的技术之路

by 刘未鹏(pongba)
原文请看这里

今天看了pongba的帖子,颇有体会。

上周末看了dreamhead的一篇文章(《起步的台阶》),有些感触,忍不住在TopLanguage上八卦了一把,抛的是砖,引的是玉,获益良多,故摘抄于此:



我:

01年进大学的时候,想学计算机,但却身在数学系,那个时候就做好了打算,数学只要混个及格,其余时间钻心看计算机的书,曾经一个月买了四百多块钱的书,结果只能吃方便面度日,大学有两三年时间,学习计算机的热情是最高的,从坐车到上厕所都带着书:P

也正是在那个阶段,学了很多底层知识,也钻了无数的细节。那个时候的信仰是:技术隐藏在细节当中。那个时候看的书,代表性的是:《编码的奥秘》、《80x86汇编技术原理》、《Windows核心编程》、《C++设计新思维》、《Inside C#》、《Linux内核源代码情景分析》……(那段时间最喜欢的就是"内幕揭秘"、"原理"、"深入"之类的书)

现在看来,这段时间学习方式的优点:钻技术细节,锻炼思考能力。缺点:一叶障目不见泰山。过分钻研技术细节导致忽视了要紧的技能;如,编码习惯非常差,以前写的代码,过了两年之后拿出来看,不忍卒读,连基本的DRY原则都没能遵循。此外,对技术细节的关注导致了忽视了一些编码方面形而上的原则。

这样的学习几乎持续到了本科的结束;其结果就是脑袋里技术细节不少,但编程方法学上的东西却几乎一概不知(比如对如何编写优质的代码的忽视、对测试的忽视、对重构的忽视、乃至对OO也只是停留在一点皮毛认识上)。

直到读研的第一年,才开始拿起《The Pragmatic Programmer》、《Code Complete 2nd》、《Object Oriented Software Construction》这些书,关注起一些"更大"的问题来,也拿起来《Programming Ruby》读一读,也正是这个时候,才算是走出了语言细节、技术细节的无尽泥沼,开始思考一些软件开发方面一般性的东西,开始意识到"脱离语言思考,使用语言实现"。四年,从纯技术的小世界中走出来,还不算晚。回头看,以前的钻技术细节的历史也并非全无用处,尤其是培养了金出武雄所谓的"思维体力",而在底层知识方面的积累也让后来看一些更高层的东西的时候心里更有底。然而,现在看来还是觉得:一,在细节方面花的时间太多了(细节的世界是无穷的,尤其是IT领域,一个老概念新翻,就能变化出无数细节。一个框架的API文档就隐藏着无数的"技术细节")。二,没有区分"非本质的细节",和"本质的细节",像硬件架构知识,属于本质细节,像语言细节知识,就是非本质细节了。前者是不变的,后者易变。三,没有在关注技术的同时,分出一部分时间来关注更大一些的主题(如编程的方法学,乃至项目管理,一个例子是,在以前,会认为语言的好坏至关重要,因为语言的世界是最熟知的,然而实际上软件开发的成本包含了更多重要的内容,甚至在语言方面还要取决于库、工具等等因素。以前看到"精巧的"设计模式觉得兴奋不已,现在则意识到,这些其实都是"非本质"细节)。

如今呢,倒是又开始觉得这些都不太重要,这些年的学习最大的收获不是学到的东西,而是学习的能力。对于软件开发这个知识无比细分、繁杂的领域,想要躺在过去学到的东西上过日子几乎是不可能的,然而又不可能把所有知识都装在脑中,所以唯有利用"元"能力:学习能力——用到啥,就学啥。毕竟,正如当初入数学系时的新生大会上,我们的系主任说的:数学都学了,还有什么学不了的呢?

老莫:

我跟计算机的缘分差不多可以追溯到公元1989年。我还是6年级的小学生,兴趣小组把我们带到浦明师范,然后玩弄了一下apple II。也就是个四则运算什么的。说真的,我是一点感觉都没有,基本上没分清计算机和计算器的差别。

此后,在中学里我参加模型小组什么的,玩的挺开心,还得了些小奖。联系了实践能力和动手能力,倒是很有好处。我的钳工手艺差不多是那时候打下的基础。高中时,参加了"头脑奥林匹克"(Odyssey of Mind),一种从美国引进的面向青少年的创造能力比赛。我们得过冠军和季军。从中我接受了分析能力和创造能力的训练(没错,这也是可以训练的),学会如何运用基本原理,创造性地解决一个复杂问题。当然,这种比赛时要动手的,动手能力进一步得到锻炼。

后来,我稀里糊涂地进了大学。(我没有夸张,我们这一届应该是全中国历史上唯一的一次,仅凭借会考成绩进大学的。没错,是上海工业大学,现在的上海大学,托钱伟长的福,祝他安康:))。

由于高中时的比赛经历,我选择了机械系。我原本喜欢物理的,不过上工大那时没有物理专业。不管怎么样,能读大学就行了。两年级开始学用计算机——AutoCAD——机械系学生的吃饭家伙。94年的时候,我母亲的一个同事的先生建议我们家买电脑,让我学计算机。至此,我才真正地接触计算机技术,一发而不可收。

先是学了dBase,然后是C。96年开始接触C++,接着一头栽进去,就回不来了。从Borland跳到VC,从 Win16到Win32,从api到mfc。啃过几个月的com规范,对于ms那套东西算是摸得很熟了。但始终没有跳出ms的范畴。而所用的C++,也不过是C(++)而已,直到两件以前,接触了标准C++和众老大的著作。一句话概括,我是一个用了12年C++,拥有2年半C++经验的程序员:P。

半年多以前,我听说了pongba和他的blog,然后发现了TopLanguage。然后么,在同众老大的学习和探讨中,进一步开拓了我的眼界,跳出了C++,跳出了ms,进入了更广阔的天地。一切还都在学习,学无止境啊。

这就是我的技术成长经历。概括起来就是两个字"杂交"。尽管我现在搞软件,一个很专业的领域,但是过去的经历,所学的知识,都有极大的帮助。有时,另一个学科(对我主要是机械工程)的只是和技能,会起到意想不到的作用。我也曾经学习了一些物理学,其中的一些营养,或许现在正在潜意识中促进了我的编程技能。:)

btw:从我现在的眼光看来,pongba的专业经历是最理想的:)。本科的数学,然后转向计算机,通讯或其他什么理工类专业,思维往往非常深刻严谨。没的话说了。:)

我和老莫:

老莫写道:

我跟计算机的缘分差不多可以追溯到公元1989年。我还是6年级的小学生,兴趣小组把我们带到浦明师范,然后玩弄了一下apple II。也就是个四则运算什么的。说真的,我是一点感觉都没有,基本上没分清计算机和计算器的差别。

我:呵呵,我最早接触计算机比老莫晚多了,是在97年,刚进高中,老爸买了一台台式机(奔腾MMX 200MHz,32M内存,2.1G硬盘),那个时候,最感兴趣的就是每天吃完饭之后去校图书馆看电脑爱好者,把里面的注册表应用技巧抄在一个小本子上,回到家里折腾:) 那个时候每个月必买的是三本杂志:科幻世界、电脑爱好者、电脑报。

老莫写道:

此后,在中学里我参加模型小组什么的,玩的挺开心,还得了些小奖。联系了实践能力和动手能力,倒是很有好处。我的钳工手艺差不多是那时候打下的基础。高中时,参加了"头脑奥林匹克"(Odyssey of Mind),一种从美国引进的面向青少年的创造能力比赛。我们得过冠军和季军。从中我接受了分析能力和创造能力的训练(没错,这也是可以训练的),学会如何运用基本原理,创造性地解决一个复杂问题。当然,这种比赛时要动手的,动手能力进一步得到锻炼。

后来,我稀里糊涂地进了大学。(我没有夸张,我们这一届应该是全中国历史上唯一的一次,仅凭借会考成绩进大学的。没错,是上海工业大学,现在的上海大学,托钱伟长的福,祝他安康:))。

我:我感觉这类经历还是相当有价值的!我本科有一舍友,高中的时候玩过竞赛,我们讨论算法问题的时候,感觉他直觉明显比我好多了。我们那个时候学校比较保守,玩的是应试教育,没有这些经历,对思维方式造成了很大的僵化影响~

老莫写道:

由于高中时的比赛经历,我选择了机械系。我原本喜欢物理的,不过上工大那时没有物理专业。不管怎么样,能读大学就行了。两年级开始学用计算机——AutoCAD——机械系学生的吃饭家伙。94年的时候,我母亲的一个同事的先生建议我们家买电脑,让我学计算机。至此,我才真正地接触计算机技术,一发而不可收。

我:老莫,我高中的最高理想就是报理论物理(握个手哇~~),一方面小时候看的自然科学的书对我的兴趣影响极大,另一方面初中的物理成绩也给了我很大的成就感。非常喜欢物理的那种深刻。但因为色弱不能报考理论物理,所以报计算机的,但差了3分,落到骗人的"信息与计算科学"(后来听另外几个朋友说也是被这个名字骗了,以为是计算机相关的系,才报的,哈哈:P)

老莫写道:

这就是我的技术成长经历。概括起来就是两个字"杂交"。尽管我现在搞软件,一个很专业的领域,但是过去的经历,所学的知识,都有极大的帮助。有时,另一个学科(对我主要是机械工程)的只是和技能,会起到意想不到的作用。我也曾经学习了一些物理学,其中的一些营养,或许现在正在潜意识中促进了我的编程技能。:)

btw:从我现在的眼光看来,pongba的专业经历是最理想的:)。本科的数学,然后转向计算机,通讯或其他什么理工类专业,思维往往非常深刻严谨。没的话说了。:)

我:^_^怎么说呢,虽然数学仅学了个半吊子,但后来我回头看,最重要的课程" 微积分"(严密思维)其实已经在大一学掉了,我记得教微积分的那位老教授的课,是相当有意思的,在整个大学生涯中,也仅有这一门课,去听过;也仅有这一门课考了90分向上。后来大三大四就是在软院读的了,偶尔跟过去的舍友聊聊数学系在上什么课,感觉后来学的近世数学就开始繁复了,不复原来的简洁优美。另外,每次考试前的包夜(临时抱佛脚)也极大的锻炼了理解力,一两个晚上需要基本搞定一学期的内容,一般我的策略就是捡最重要最本质的内容理解(而不是记忆),其余的就管不着了,结果每次都混个过关:P

虽然最终数学没学到太多东西,但在严密思维以及理解力上面的锻炼还是相当有益的;或许,正如系主任所说,在学新东西的胆量上面,也有作用:-)

2009年1月2日星期五

火车票哪去了

从新浪转载过来的,原文在这里,文字不怎么样,但是说的却是事实,这种事我也碰见过好多次。

2008年12月30日凌晨3点,在男生寝室熬了一晚上的4人偷偷的钻出教学楼,1台笔记本电脑2块电池1张桌子4个凳子就是我们全部装备,虽然装备夸张但为了度过8点卖票的5个小时我们也想不得那么多了。学校里我们算比较早的所以排了个好位置之前来的只有一个人,两方各自在一个窗口排队,外面气温很低可我却丝毫感觉不到,或许东北的棉衣特别保暖或许我俨然触摸到了到家的希望。

7点03分,一位兼职的同学叫售票阿姨开门,这支 2对足有150+而且多数都等待了3个小时左右的队伍似乎开始有些躁动,我和我后的刚认识的一个西安的学长更是激动,或许只是因为我们回家的路程都要24 小时一样,我们格外聊的来。这时候我代表一起的7个东北同学(4男3女),看到自己的好位置不由感叹一晚上的冷风没白喝。

7点31 分,由于窗帘的宽度有限,我透过露出的一丝缝隙加上一个不近视的眼睛(我相信即使是近视只要不是太高或者是没带眼睛的情况下也能看到)离我位置2米左右的位置有一打一打的火车票约30张。或许有些人提出质疑:"火车站的票不是8点才卖么?"对此我只能说我和你一样疑惑。我不知道为什么这时候会有那么多票, 可是当场和我在内至少4名同学看到这情景。

3至4分钟之后,售票阿姨扯开窗帘,(约7:35左右)头都不抬下还来不及坐稳可能都没碰下她手边上的电脑,(解释下售票点2太电脑,一台是兼职学生用的,我排在另外的一个窗口,当时她们打票用的是兼职学生的电脑所以才可以有个角度看到。之所以这样或许可能是为了事情暴露是有个借口,当然这只是我的猜测)直接说:"西安和沈阳北方向的票卖光了"。

很快我和售票阿姨发生争执,措词强硬表示里面有票,我已经看到了,气氛紧张。售票阿姨迅速表示要叫保安,还说去年就是我,说我只会无理取闹。(再解释下,去年我也是早早的排在队伍的第一个,她当时带了个男的进入售票窗口内将票带走,被我告发到铁路部门,事后她不知道如何得知我的电话号码给我打电话要给我和当时在队伍里一起的若干学生带票,希望息事宁人。因为我认为是我应得的所以我就收下了,本以为她不会如此,真是让我大跌眼镜当然我是不带眼镜的,同时也为我没有继续告她而失望)我更是要求叫110,同时她却说处写什么我不感激她,并且告诉我10号以后都没有票,态度极其肯定。我很不解,她一年前打出去给那个买走后门的买票大户男人,当时那男人带出来的不会少于三四十张,有隔壁学校的一起买票的2学生见证,也是他们告诉打出来票和此男子有违法违规嫌疑,此男子在没有证据面前我姑且不称其为票贩子。那男人拿走的本应该是我们的票,是我排队买票应得的,为什么要感激她???并且叫嚣说我可以再去告他,铁路局铁道部也可以。同时打电话给某个称呼为小赵的人。我猜测可能去打通关节或者是恶人先告状,我是发自内心的希望是第二种可能。

之后的若干分钟由于我处在情绪不稳定情况下,没注意具体时间,一穿白衣服女子从售票点走出匆匆走掉了,但是这一幕没有逃出我们同学的眼睛。

8点卖票之前售票阿姨走掉,我们试图跟阿姨沟通,表示我们一定要我们应得的票但没有结果。

今天是元旦新年,相信很多人度过了个美好的一晚还在休息,可是我却是奥了一个晚上可是回家的票还没有着落。刚才看到新闻,08年末灾区孩子告别板房,心里很不是滋味虽然很累虽然我的打字速度是在算不上快和准确,可我还是要把我的经历写下来。或许我跟灾区比真是太过于矫情了,可是我看到漫天和我一样正经排队买不到票,可是越来越多的“火车票购买服务者”放出豪言,“要多少张吧”。我真是没办法心里不由的羡慕下他们,至少在新年即将到来之际貌似还有人惦记,貌似不想现在的我们"死不死谁儿子”,可是我和希望一起坐火车走的3个女生,还有整个学校来想回家的,整个南昌打算过年回家的人,所有对火车票望眼欲穿的人。我想带大家问一下,“我们的火车票哪去了?”

现在我对我能不能在春节回到家已经感觉到怀疑了,难道所有想买票的人都应该去黄牛党手中买票?????回来跟学校某老师反映,简单明了的回我几句话“这也不是我管辖范围之内不是,你们去找铁路部门,我还在外边。。。” 难道我们出门在外的学生就没活路了?????难道这就是成熟要学习的代价?????难道我要适应这些才叫成熟?????难道这就是社会现实的一面?????难道就因为我们没后台就成了死了都没人管的人????或许只有我成为他麻烦的时候才会来“耐心教导下”。。。。

呜呼哀哉

我知道我是告不倒售票阿姨的,我相信公正,相信公正或许存在。。。我认为大家有个公正的评价,我也幻想着坏人得到应有的处罚,幻想闪电来闪电去的严打能更日常化,幻想我们的每个人都能方方便便出行。

我还要对那名老师说,或许你管不了,但我们都知道你的位置不算很低你说话要比我们更容易受重视,或者你不想去趟着浑水至少对我们给予一定的安慰不应该说这些几乎等同于“我不管”的代名词。还有我很抱歉打扰了你“极其重要的出差”

回想一年半以前,平平淡淡的夏日清晨但如今想起来却让我不禁冷飕飕。我倔强的踏上由家乡长春通往南昌的行程,毫不犹豫,充满向往,我的目的地是南昌的某较知名的大学,可如今我的心情今非昔比或许我高考考试差些留在东北,12月的雪我会感到更温暖。