程序员的语言“艳遇史”(七)——电台播音员scheme
第七个女孩 电台播音员scheme
(***以下小说情节纯属虚构,供朋友们在紧张编程后轻松一下,如有雷同纯属巧合,切勿对号入座***)
一、天籁之音
早班车上,每过一站,都是一阵汹涌的人流波动。我好不容易抢到一个座位,环顾四周,没什么老人孕妇,窃喜。最近一周倒霉,每次才占到空位,都迫于群众压力,当了雷锋,连假装看窗外都不成。我放心的坐下,随手抹了一把汗。其实我本可以坐11路车上班,路程还少两站,也没这么挤。可是那路车整天播新闻广播,主持人声音和破锣似的,哪能和音乐交通广播scheme姐姐的天籁之音比啊。
不错,7:30节目准点开始,车上伴听传来那个我熟悉的女声,“听友marry通过本节目给她的男朋友点歌一首,她在发来的短信里说,漫漫人生路,有你长相伴……要点歌的朋友可以发短信到……”。我急忙掏出手机发短信,不用主持人说,那个SP特服号我太熟了。为什么呢,本公司正是音乐交通广播唯一指定短信合作伙伴,节目流程还是我设计的。呵呵,就算当公司产品的义务测试员了。就是短信信息费太贵,靠,一条5元,跟抢劫没什么两样。改天找我们麦总报销。
二、 麦总是谁?
麦总就是我大学那个老同学麦东啊。我就是受到他的邀请,才到现在这家公司的。大学毕业时,麦东一点都不为工作着急。我当时还百思不得其解,现在我才明白,还找什么工作啊,人家是为社会创造就业机会的层次。麦东老爸是副市长,有这个政商背景在,还有什么搞不定的。
麦东毕业后,玩了几年,被他爸骂得够呛。于是就借钱搞了一个从事电信增值业务的公司。公司初创缺人,他就想到我这个大学“同居”多年的死党,叫我助他一臂之力。我正好做网络集成这个行业做累了,和领导关系也不好,想换换口味,就离家到了这个陌生的城市。
到公司一看,麦东这家伙,还和当年一样色。才聊几句,就和我对外面女秘书的身材进行一番研讨。终于谈到工作,他决定委以我公司技术总监一职,命我即刻上任。我开始还假意推辞,到技术部一看,就一个维护机器的小弟,一看名片,还是技术部副总。四周再逛了一下,公司就十来个人,称呼都是某总,连做卫生阿姨都是保洁部主管。我大怒,整一个皮包公司嘛,准备不干了。麦总急忙挽留,说就当到这里旅游,也得呆三个月,否则老同学情意就没了。
后来我才知道,对短信SP这个行业来说,技术不是主要因素,关键还是有没有信息源,和运营商关系铁不铁。我们麦总年轻有为,什么中高考查分、电台电视台节目互动,纷纷抢到手里。以前民国时候,形容军阀有一句话“大炮一响,黄金万两”,我们公司的炮弹就是短信群发。那时每上一个新节目,我最喜欢的事情就是趴在服务器终端前,边看用户短信流量边流口水。一条、二条……一万条,那就是钱啊。我和麦东经常取笑那些在.COM公司的兄弟,它们有盈利模式吗?看看我们,已经搞定。现在我再也不嫌人少了,麦总催了我几次,叫我搞搞招聘。我都给他顶回去,说忙得过来。招人,我傻呀,人少才好分钱啊。唉,当时真是小农意识浓郁。
三、和SICP的相遇
一天,我闲得无聊,就去逛新华书城。忽然耳边传来了那个熟悉的女声,“现在我为大家献歌一曲,高天上流云……”。这不是音乐交通广播的scheme吗,她怎么也来书城了。我和没头苍蝇一样,在书城里四处乱窜。后来才搞清楚情况,原来书城五楼正在搞音乐交通广播听友见面会。等我赶到,已经曲终人散。唉,错过了和scheme姐姐见面的机会。
我正郁闷着,随手抓起一本别人不要的丢在书架上的书。哈哈,没想到情场失意,淘书竟然淘出一个宝贝。那就是大名鼎鼎的SICP,北大裘宗燕老师的译本《计算机程序的构造和解释》。以前一直听说过Lisp这门古老的语言,可是总不得其门而入,今日终于得偿所愿。
该书使用的程序设计语言是上个世纪70年代诞生在MIT人工智能实验室的Lisp方言,名为Scheme。它的发明者是Guy L.Steele和他的老师Gerald Jay Sussman。Scheme语言秉承了Lisp传统,采取了基于表达式计值和函数施用的计算模型,而且无论程序还是数据都具有一致的S表达式语法。
虽然Lisp方言很多,但Scheme绝对是有特殊历史地位的一个。Scheme比其他早期lisp方言具有更坚实的理论基础,也可以说更“纯”,它的许多概念奠基于数学里的无类型Lambda演算。虽然Lisp也是来源于此,但它的许多方言并不严格遵循该理论。Scheme语言把词法作用域引入了Lisp家族,而其他的Lisp方言除了后来的Common Lisp外,大都采用动态作用域。Scheme语言还鼓励大家大胆的使用递归,而不用担心堆栈溢出,因为它把尾递归优化作为实现标准。
SICP这本书(Sussman还是作者之一),实际上就是他们Scheme语言设计和研究的一个缩影。书的难度确实不低,我当时了解到这竟然是MIT入门课程6.001的教材,简直震惊了。难道中美计算机科学教育的差距有这么大吗?后来我在“Joe on Software”里看到Joe对这本书及相关课程的说法:(译文摘自《More Joe on Software》中文版《软件随想录》中的“Java语言学校的危险性”)
“我读的这门导论课,是宾夕法尼亚大学的CSE 121课程,真是读得苦不堪言。我注意到很多学生,也许是大部分的学生,都无法完成这门课。课程的内容实在太难了。我给教授写了一封长长的声泪俱下的Email,控诉这门课不是给人学的。宾夕法尼亚大学里一定有人听到了我的呼声(或者听到了其他抱怨者的呼声),因为如今这门课讲授的计算机语言是Java。”
看到这里,心里平衡了不少,因为我清楚的记得我在大学一二年级的水平。不过SICP这本书对Scheme语言的介绍只能算是中级,它的重点是编程思想和计算模型,而非语言本身。从语言角度讲,它至少忽略了两个重要主题:宏Macro和延续Continuation。从理论角度讲,它并没有多讲Lambda演算方面的内容。如果把这本书当做入口,再深度钻研下去,你就会发现一个更宏大的领域——函数程序设计FP。
四、仰望FP星空
函数程序设计作为一场运动,已经统治欧美程序设计语言理论界多年。如果把FP当做一个武林门派,那开山鼻祖就是逻辑学家Alonzo Church。他在1941年发明Lambda演算,这是一个研究可计算函数及函数施用的形式系统。上个世纪50年代,John McCarthy和他的小组发明了Lisp语言,把Lambda演算引入了计算机世界。到了70~80年代,FP终于成为显学。连命令式语言的老祖宗Fortran创始人JohnBackus都倒戈了,他在1977年发表了著名的图灵奖演讲“Can Programming Be Liberated From the von Neumann Style?”,大力鼓吹FP。FP分支众多,目前比较流行的程序设计语言有Lisp/Scheme、ML、Haskell、Erlang等。最近连微软这样一向保守的工业主流企业,也借鉴ML语言出了F#这样的东西,这在以前想都不敢想。
那时我也是附庸风雅,根本不是学术界的一份子,竟然叛变革命,也开始看函数程序设计方面的文献。我下载了Steele和Sussman为Scheme语言发表的一系列论文,这些文章完整讨论了Scheme的语言设计、解释器实现、编译器实现,甚至一个Scheme硬件处理器的实现,堪称计算机科学技术的学术典范,名噪一时。每篇论文都以Lambda作为题目开头,比如“Lambda: The Ultimate Imperative”、“ Lambda: The Ultimate Declarative”等等。此风一起,后有好事者加以模仿,在写Scheme语言或其他函数语言编译技术有关的论文时,也常用Lambda开头。国外一个讨论程序设计语言的学术网站,起名叫Lambda the Ultimate,看来也是受此影响。
一个晚上,我读论文读累了,走到房间阳台透一透气。小区里的空气是闷热而污浊的,空调外机响着,楼下几个大排档人头耸动,吵得要死。我如同一个文艺青年,抬头仰望星空,回想函数程序设计60多年的历史,心潮澎湃,颇有众人皆醉唯我独醒之态。忽然接到麦东电话,叫我一起到某新开酒吧happy。这帮俗人!我坚决予以拒绝,决定今夜青灯古佛,陪论文度过。麦东使劲诱惑,“来吧,我还请了音乐交通广播的几个美女,嘿嘿!”。什么,音乐交通广播,美女!我脑中立刻想到了scheme姐姐,立马把论文丢在一边,直奔酒吧而去。到了才发现此美女非彼美女,是电台广告部的几个人,又被骗了!
五、何为情商
麦东这个人,学习一般,对学术问题更没什么兴趣。他学生时代就是我们班一活宝,专业追女,业余学习。毕业论文更是一塌糊涂,后来兄弟帮他捉刀一篇,几乎逐子逐句改过,才过了关。也许就是这份交情,他后来开公司才想到我。但是和他相处一段,我不能不佩服他的情商。所以人家是老板,我们是打工仔。
就拿和运营商公关一事讲吧。联通最近部门调整,新成立了增值业务部,我们去拜访对方新领导。车到联通大楼门口,麦东鬼鬼祟祟的换了一部手机,名片也换了一个版本。我极为不解,干嘛?麦东白了我一眼,说“你真木。到联通公司,你总不能拿着一部中国移动的手机吧。这让别人不爽。”。我们公司和三个主流运营商移动、联通、电信小灵通,都有SP接口。麦东给业务部同事和他自己准备了三套手机、三种名片,看人摆碗碟,真够绝的。
出去商务谈判,坐在麦东旁边,差点把我吓出心脏病。整一个卖拐的大忽悠,上到音信互动节目,下到网络布线工程,没有不能做的。我看要是对方讲开发航空母舰,估计他眉头都不眨,也能应承下来。有几次我在桌下,直踢他脚,他还滔滔不绝,口若悬河,弄得我狂汗。后来麦东和我讲,这就叫气势,商场中没这个自信,人家看不起的。我反问,要是公司没能力开发怎么办?麦东说转包啊,能拿到单子才是爷啊,后面做工程的和乞丐一样一大溜。我哑口无言。
每年315消费者权益日,我们都特紧张。就怕某个用户乱投诉,媒体来个曝光,公司就惨了。那时我们还是很守规范的,骗都是骗,但是用户订购的上行短信都还是有记录的。但是有的用户对资费和服务就是不认可,运营商分钱没问题,投诉全往我们头上推。我们技术部还认死理,和用户比对短信记录,差点没吵起来。麦东多次力挽狂澜,花钱消灾,把用户伺候得服服帖帖走人。这就是功夫啊。他还发动关系网,建立投诉预警系统,各大媒体、消协加工商局,四处打点,防范于未然。
有这样的老总在,下面工作好做多了,我这个技术总监也日渐轻松。SP短信技术系统还是比较简单的,我借鉴SICP书中介绍的解释器手法,偷空开发了一个简单的DSL,这些就可以不用C语言做流程了,以后上新节目让小弟编个DSL脚本就行了。温饱思淫逸,我有时上班空下来,不务正业,也研究研究Scheme语言。
六、从理论到机器的旅程
函数程序语言研究群体毕竟是计算机科学家,不是搞纯数学。您鼓吹了半天,总得有个在机器上可运行的东西出来吧。所以我们的理论家也不得不脏一下手,考虑一下编译问题。他们的套路和传统命令式语言的编译技术是极为不同的。很多函数语言编译器开发者经常分两个层次考虑问题:
1、如何把各种各样的语言构造,统一归结为函数计算模型,即转换为lambda演算形式;
2、如何把lambda演算形式及其归约操作,在主流von Neumann机器这个异构的设施上有效予以实现。
在这些问题上,无尽的努力已经耗去了,多少人白了少年头。下面我们看看,在经典的Scheme语言实现中,是如何处理这两个问题的。
Lisp传统中,由于数据和程序表示的一致性,程序的转换可以通过数据结构的操作来实现。由于可以用read原语像输入数据一样输入S表达式,不用再进行语法解析,在Lisp中编写解释器或编译器是一件很容易的事情。Lisp中经常用于程序转换的手段称之为特殊型。首先可以实现很小一个子集的特殊型,然后利用这个子集再去定义新的特殊型。在解释器或编译器的处理中,在此子集合外的特殊型被翻译成预先定义的特殊型,然后再对变换后的表达式进行操作。新定义的特殊型称之为宏,翻译过程称之为宏展开。
所以对于第一个问题,Scheme的回答就是把lambda演算形式作为核心子集,而其他的程序构造定义为语言的宏。
看两个简单的Scheme语言程序。
程序一:引入局部词法环境的let表达式
(let
((a 1) (b 2))
(+ a b))
它被宏展开后,可以转换为类似这样的一种形式:
((lambda(a b) (+ a b))
1 2)
可以看到,变成了一个lambda表达式对常数1和2的施用操作,结果是3。
程序二:引入副作用的begin表达式和set!表达式
(define a 0)
(define b 0)
(begin
(set! a 1)
(set! b 2)
(+ a b))
其中的begin表达式被宏展开后,可以转换为如下一种形式:
((lambda(x)
((lambda (y)
((lambda (z) z)
(+ (getbox a) (getbox b))))
(setbox! b 2)))
(setbox! a 1))
变成了三个lambda表达式的连续的施用操作。可以看到x和y变量实际上是没用的,(setbox! a 1)和(setbox! b 2)两个表达式的返回值被抛弃了,只有最后一个(+ (getbox a) (getbox b))的值作为整个表达式的返回值。所以,如果begin表达式中的子表达式没有副作用,那对整个程序的计算是没有意义的。
还要注意的是对set!赋值表达式的转换。如果不对set!表达式进行处理,变量值允许变化,就无法保持lambda演算理论的纯洁性,也无法对程序进行数学语义推理了。
这里变的戏法是为被赋值的变量引入一个间接层,把对变量的赋值和引用转换为对内存数据结构的读写原语getbox和setbox!。变量指向该内存数据结构,在程序运行时间内不再改变,这样在形式上就符合lambda演算中变量一经绑定不再改变的特性了。
同样,Scheme语言中其他特殊型比如什么let*、letrec、and、or、cond、case、do、define,它们可以被宏转换成quote,setbox!,getbox,if和lambda这些核心lambda演算的形式。
下面解决第二个问题,lambda演算这种可计算函数的模型如何能在状态转换的底层机器上跑起来。
现在忘掉计算机,回到初中时代,怎么算三角形面积?底*高/2,表示成代数式:
a * b / 2
代入实际数值就可以计算了,有的人先算a*b,然后再除2;有的人先算b/2,然后再乘上a。都可以。如果让计算机这个电子傻瓜帮你算,用贴近机器的汇编语言,你就得明确计算顺序。我们用CPS(Continuation passing style)形式的Scheme代码来显式的表达计算顺序。
一种是( * a b (lambda (x) (/ x 2 (lambda (z) z))))。
另一种是( / b 2 (lambda (x) (* x a (lambda (z) z))))。
注意*和/的操作类型不再是 number number -> number,而变成了number number continuation -> number 。多了一个continuation延续参数,*和/的结果不是立即返回,而是作为参数传给continuation,使得计算继续持续下去。现在这个式子,不仅是一个和原来的代数式语义相同的代数式,而且可以被看成凸显控制流的中间代码。
我们就第二个式子继续变换:
j = (/ b 2 (g a))
g = (lambda (y) (lambda (x) (* x y f)))
f = (lambda (z) z)
这里生成了三个新表达式,注意原来的两个continuation不再是匿名函数了,而被提升到全局环境,分别用f和g引用。注意这里的函数g,它对应于原来的(lambda (x)(* x a (lambda (z) z))),多了一个参数y,代替式子里的自由变量a。
假设有一个堆栈机作为目标机器,其中push是入栈指令,div和mul分别是除法和乘法指令,goto是跳转指令。我们就可以把上面的中间形式转换为汇编伪代码:
j: // 汇编语言标号
dd a … // 这里定义了两个变量
dd b …
push b
push 2
div
push a
goto g
g:
mul
goto f
f:
注意这里所有对延续的函数调用都属于尾调用tail call,在编译处理上都不生成保存函数返回地址的代码,直接goto到一个标号地址即可。可以看到,代码还可以再简化,很多编译器生成的指令是不必要的。这就是两个不同计算模型转换的代价,我们这个例子太小,还不怎么能说明问题。
以上我们杀鸡用牛刀,展现了经典Scheme语言编译器如何把一个弱智的lambda表达式转换为机器代码的过程,演示了每步的程序形式。在其中的第一步和第二步分别使用CPS变换和Lambda Lift变换,最终生成了虚拟堆栈机的代码。这些程序变换是否语义上与原来一致呢,这是一个关于可信编译器的证明问题了,太麻烦了,还是留给那些计算机科学家吧。
七、离别的日子
真是世事无常,我们的好日子才几年,就结束了。麦东他爸因为房地产土地买卖的问题被双规了。在此打击下,麦东失魂落魄,也不怎么来公司了。业务虽然还正常运转,但已不比从前。中高考查分信源丢了,今年移动的SP新节目报批也迟迟没有下来。整个行业背景也在变化了,移动梦网的新政即将出台,将来的监管力度会更严。苦撑着没多久,麦东和我做了一次长谈,决定把公司卖了,出国算了。我当然支持他了,都是兄弟嘛。我决定渡过交接期后,也辞职回家。
在即将离开这个城市的时候,我有一桩未了的心愿。大家都猜到了,那就是无论如何要见scheme姐姐一面。三年了,天天在公车上听到她的声音,她就好比我身边的一个朋友。我决定不再错过,通过电台活动部的小张,我混进了广电大楼。
站在播音室门口,我坎坷不安,手汗都把日记本弄湿了。突然门开了,一个身材臃肿的中年妇女出现在我的面前,穿着一件普通的黑裙子,头上戴着我妈那个年代的发夹。
“你好,我是scheme。”
后面的几分钟我恍惚梦游,带着scheme签名的日记本,我走出了广电大楼。看见了满天星光,我不禁哑然失笑。从旁边的商铺里传来那熟悉的歌声:
“千万里,我追寻着你,
可是你却并不在意,
……”
posted on 2010-07-30 14:37 没一句正经的业余程序员 阅读(6164) 评论(26) 编辑 收藏 举报