June 25, 2007

各个语言编程的风格

续 Re: 各个语言社区的风格。

不和任何编程风格相绑定的语言是玩具,例如 Lisp/Scheme、Perl、JavaScript,就好比法杖,拿来敲人不行,但往往可以拿来放魔法;
和两种编程风格相绑定的语言是怪玩具,例如 E、Ruby,就好比竹剑,拿来劈人吧,勉强;拿来放魔法吧,凑合;
只和特定的一种编程风格相绑定的语言才是常规的武器,这你可能会想到很多,但也有些区别:
  • 和一种与设计原意相背离的编程风格相绑定的语言是怪武器,例如 C++、就好比吴钩,杀人的动作既不是砍又不是刺,是拉,用起来很别扭;
  • 和一种与设计原意相符合但是过时了的编程风格相绑定的语言是笨重的武器,例如 C、Java/.Net,就好比铡刀,那边要杀人了,非得把敌人逮着绑好了推到你手边才能动手,步骤繁琐;
  • 和一种与设计原意相符合并且有理论支持的编程风格相绑定的语言才是好武器,例如 Haskell、ML/OCaml、Erlang、Python,就好比刀剑,拿起来就能用,用的时候不是砍就是刺,干净利索。
一般来说,我倾向于两种极端:彻底的玩具和优秀的武器。

June 24, 2007

Re: 各个语言社区的风格

Scheme/Lisp:我们是研究空气动力学的。
Smalltalk/Self:我们是设计飞机的。
Lua/JavaScript:我们是修理飞机的。
C/C++:我们是造飞机的。
Prolog/Planner:我们是开飞机的。
Python/Ruby:我们是卖飞机的。
Java/.Net:我们是打飞机的...

FP 做数学统计题

今天布置的家庭作业还真是烦人!做完《数学之友》上“统计案例”一章所有习题。我的妈呀,算什么卡方、线性回归,公式繁琐地要命,按计算器都能把人按死。还是让电脑帮我做吧。
不用什么 GNUplot、Mathematica,俺用 Haskell,自己动手,丰衣足食!

一. 独立性检验
卡方就是一个公式:
  
kaf a b c d =
  ((sqr ((a * d) - (b * c))) * (a + b + c + d)) /
    ((a + b) * (c + d) * (a + c) * (d + b))

-- sqr 函数是用来求一个数的平方的,再实现一下:
sqr n = n * n
  
这样就搞定一种题了!以后算卡方不要太简单,把要检测独立性的四个数据按顺序作为 kaf 函数的参数输入 Hugs 就搞定了!
Main> kaf 184 61 91 9
11.097807845216 :: Double

二. 线性回归分析
其实就是求一个 y = a + bx 中的 a,b。把 xn 值、yn 值各表示为一个列表,像这样来使用:
Main> lrb [1..8] [5.54, 7.52, 10.02, 11.73, 15.69, 16.12, 16.98, 21.06]
2.12142857142857 :: Double
Main> lra [1..8] [5.54, 7.52, 10.02, 11.73, 15.69, 16.12, 16.98, 21.06]
3.53607142857143 :: Double
那么,就可以根据公式把它们抄出来:
  
lrb xl yl =
  -- 这里要注意一下,数学中的 sigma 是一种记法而非函数
  (sigma
   (\(x, y) -> (x - xp) * (y - yp)) (zip xl yl)) /
    sigma (\x -> sqr (x - xp)) xl
  where
    xp = avg xl
    yp = avg yl

lra xl yl = avg yl - (lrb xl yl) * avg xl
  
其中,avg 函数用来求一个列表的平均值(这里也就是所有 x 取值的平均值啦),它需要知道列表长度:
  
avg ls = sum ls / len ls

len [] = 0
len (x:xl) = (len xl) + 1
  
最后一个 sigma 函数的实现也很简单,就是累加嘛,只是写地有些不好看罢了:
  
sigma f (x:xl) =
  if xl == []
  then f x
  else f x + sigma f xl
  
重申一遍,这里的 sigma 被表示为函数而非数学形式。那么,猜猜下面的 sigma 的数学形式是什么?
Main> sigma (\(x, y) -> x * y) [(1,4), (5,3), (3,8), (2,4)]
51 :: Integer

三. 线性相关系数
线性相关系数 r 的求法也只是个函数,再抄一遍:
  
lrr xl yl =
    (sigma
   (\(x, y) -> (x - xp) * (y - yp)) (zip xl yl)) /
    sqrt ((sigma (\x -> sqr (x - xp)) xl) *
     (sigma (\y -> sqr (y - yp)) yl))
  where
    xp = avg xl
    yp = avg yl
  
以后这样用就行了:
Main> lrr [1..8] [5.54, 7.52, 10.02, 11.73, 15.69, 16.12, 16.98, 21.06]
0.987345979074916 :: Double
哇!这组数据的线性相关性接近 1,好高啊!

四. 结语
现在发现 Haskell 还真是实用,这个周末数学作业不愁了,打魔兽去喽!
最后给楼下的看客留一个与 FP 没什么关系的小题目:
对于一组线性相关数据,往往要求它的所有产生数据—— a、b、r,还有线性回归函数,顺便还要用这个函数再求一下对未来的预期。我如果当真输入 3 遍数据然后手工计算预期岂不是很傻?!想想看怎么让我只输入一遍数据就得到所有的产生数据(设计一下数据结构而已)还有线性回归函数(其实只有这个才有那么一点点技术含量)。

June 12, 2007

胡侃:面向对象思想的进化

本文作者还是那个无名小辈 lichray。他在考查了一些语言和历史之后,觉得有必要谈一谈自己对面向对象思想的一些诡异的想法。文中会提到许多编程语言,不过当然了,重点在于思想,文章不是用来推销语言的。


面向对象编程思想的提出已经不是几年而是几十年了,考查其思想的变化,一方面是对现有语言的一些评判,另一方面,也算是对前辈计算机科学家的缅怀。 ——题记

Kristen Nygaard在1962年发明的 Simula 语言现在被认同为世界上第一种明确实现面向对象编程中某些“必要”元素(比如 class)的语言。Simula 是从 Algol 发展来的,可以说,是一种增加了 class 这个数据类型的 Algol,并将参数传递的默认模式从“按名调用”换成了“按引用调用”,还提出了根据类型确定初始化过程的方法。
从 Simula 的时代开始,科学家们在解决软件复杂度方面的思路开始“异常开阔”——当然也有资深的老派牛人们不这么认为。比方说 Peter Norvig,写了[url="http://norvig.com/design-patterns/"]Design Patterns in Dynamic Programming[/url]一书来反驳。他认为设计模式早已体现在以 Lisp 为首的一批语言中了,根本不需要什么面向对象。但我还是跟从偶像 Alan Kay 的观点。某些时候,把一种特定的编程风格做进语言里也是必要的。
言归正传。Simula 之后的语言开始试图把 class 作为编程的基础,并提出了一系列出色的、基于类的编程风格,最终确定了“万物皆对象”这一面向对象理论的“终极思想”。于是,Alan Kay 的 Smalltalk 在 70年代诞生了。它是世界上第一个真正把面向对象作为程序组织基础手段的编程语言。它首次明确实现了“消息”和“继承”这两个重要概念,对于“封装”和“多态”也给出了里程碑式的解决方案。在 Smalltalk 程序中,一切程序元素(除了词法元素)都是对象,一切操作都是消息。而且,Smalltalk 的实现本身就是用 Smalltalk 写成的,这也就意味着,对于一个 Smalltalk 程序来说,它的底层也是以面向对象为基础的。直到现在,也只有 Self 语言有资格在面向对象的程度方面与 Smalltalk 一较高下。
Smalltalk 在成为里程碑的同时,也成为了分水岭。Smalltalk 之后的面向对象语言的发展方向分成了两条路:一是继续把 Smalltalk 中封装的概念扩大化,为程序员添置更多的约束,以支持所谓的“大规模开发”的需要;另一方面,一些人主张继续理解面向对象的原理,为这一思想添加数学上解释,而不仅仅是“拿来主义”,使程序员更不易出错或“分神”。
正像大家所猜测的那样,前者指的是以 C++ 为代表的、坚持以命令式语言风格去实现面向对象的“有类”语言,后者是以 Self 为代表的、准备以函数式语言风格去理解面向对象的“无类”语言。
有类语言中,C++ 是曾经的“蔚然大宗”。它最初的名字是 C with class,很显然,一开始只是想对 C 作出像 Simula 之于 Algol 那样的修改。但是,C++ 诞生的时候(1983年),也是 Smalltalk 80 产生的时代,也是 Ada 语言正在开发的时代。在享受了 Smalltalk 提出的类方法、对象方法、私有方法等等语言设施之后,C++ 提出了3个为后来的有类语言广泛采用的关键字:private,public,protected。不避讳的话,可以简称3P。这种区分相对于 Smalltalk 来说略少了一点,但事实证明,在实际的抽象中是够用的。另外,C++ 还从 Algol68 (这只是一个失败的旧事物罢了)那儿学来了运算符重载,还有 Ada 中的泛型和程序包。堆砌了太多特性的 C++ 显得有些不堪重负,而且失败的教育(总是拿它当 C 的升级版讲解!)、失败的实现(尤以 Visual C++ 为甚)也是它后来在开源界不那么受欢迎的重要原因。C++ 之后的有类语言开始了消减多余特性,理清思路的努力。
紧接着 C++ 之后出现的语言主要有 Objective Pascal 和 Objective-C。后者是 Mac 公司的拳头开发语言,前者是什么?就是 Turbo Pacal 中的 Pascal 语言。二者试着给古老的语言们添加新的元素。前者只是很客气的添加了 new 和 unit,特性不足;后者的转型是成功的,但说句题外话:想象一下 C 的基础上直接加上 Smalltalk 的消息传递语法,这就是 Objective-C 的语法设计。
Java,原名 Oak。这个名字现在已经处于“无敌”状态,去年已经在语言排行榜上 K 掉 C 当上了老大。Java 没有什么新东西,正如上文所说,只是一个消除了多余特性后的产物。成功的原因,一是时代的召唤,而是官方实现的基于虚拟机的跨平台特性。虚拟机早已不是什么新事物了,不过有大公司出来宣传确实是新鲜事。也许是受了微软的影响,大多数普通程序员更愿意那种喜欢被某个大公司“罩着”的感觉,于是纷纷用起了 Java。历史就是这样,听不到你的嗟呀。
Java 火起来了,各大计算机方面的媒体上充斥着 Java、OOP 等等的字眼,仿佛面向对象只剩下 Java 一种模式。各种语言纷纷改头换面,以适应“新时代”的要求:Perl 加上了4P(package 也算一个);Objective Pascal 摇身一变成了 Boject Pascal,再改名 Delphi;Basic 名字前加上了 Visual,后来后面又加了 .Net;C++ 把 ++ 改成了 # 号;就连一向对面向对象嗤之以鼻的 PHP 都被迫升到了 4.0、5.0;我的“母语”Action Script”版本也升到了2.0、3.0。
在有些人眼中,这是一场“意义重大”的变革;但在我眼中,这是一场灾难。
因为,在某个角落,无类语言们正在为寻找面向对象的数学理论基础而努力。为什么要寻找数学理论基础?这个问题早在70吗=年代就已经不是个问题了,而现在居然还有人不明所以。有类语言和无类语言的区别,就像是命令式与函数式之间的区别,就像是图灵机和 Lambda 演算之间的区别,就是一字一次的编程和面向整体操作的区别,就是 Bug 多和少之间的区别,就是单线程与高效并发之间的区别,就是必将灭亡的旧事物和必将走向辉煌的新事物之间的区别……这么多,还不够吗?
无类语言从已有的 lambda 演算理论中寻找适合解释面向对象思想的部分。1986 年研究完成的 Self 语言首先抛弃了 class 关键字,从对类和对象这两个基本问题上做文章。“万物皆对象”,类就是对象,但用它可以产生对象;怎么产生?通过复制已有对象产生。这种手段称为“基于原型的面向对象程序设计”。Self 之于无类语言,相对于 C++ 之于有类语言。它是无类语言中的蔚然大宗。但可惜的是,Sun 公司的 Self 实现几乎没有进步;Smalltalk 可能因为受够了 Java 之流自称继承了自己,认为 Self 是它唯一的“知心朋友”,发起了一个叫做 Morphic 新体系,算是对经典的延续。
Self 之后,1993年出现了 Lua。Lua 是无类语言的一个特别“函数式”的版本,在继承了 Scheme 所有思想之后,消灭了专有 List 这一数据结构,全以哈希表代之,并出色地解释了许多程序表示上的一些不太明朗的问题,还区分了消息发送和普通函数调用。Lua 不论从那个角度(科学或者商业)来看都是成功的,是无类语言中的佼佼者。
Lua 之后出现了 JavaScript,原名 LiveScript(1995),国际标准收录名为 ECMAScript。JavaScript 与 Java 诞生于同一时代,生不逢时的同时又生而逢时。说它生不逢时,是因为在 Java 的盛名之下,不负众多程序员的“众望”,被他们指责为“假面向对象”;说它生而逢时,是因为,它总算没在一浪高过一浪的 class + 4P 的嚷嚷中倒下,成为我们最为熟知无类语言。它实在是太优秀然而又太谦虚了:它秉承了 Lisp 家族的一贯传统,能够用数据表示程序本身(JSO);它又有足够的函数式编程特性,但它谦虚地称之为 function;它用复制对象再用 new 关键字 apply 构造函数的方式漂亮地解释了类和对象的关系;相对于 Lua 取消了消息发送和函数调用之间的界限;在没有任何 4P 关键字的情况下还能用逃逸变量理论实现 4P 的所有特性(不要被 dojo 之类的库蒙蔽了双眼)。几乎是一个完美的 Self 的继任者,但还是那句话,真正会 JavaScript 的人不多啊。
至此,无类语言的发展似乎已经很令人满意了,理论似乎已经成熟,只剩下一个问题:函数本身是一个什么样的对象?E 语言(1987,提出很早,实现太晚)回答了这个问题。答案很简单。函数是对象的一种,对象和方法都是 lambda 算子,如果你愿意,又可以把函数的函数体再表示为对象的一个方法!看起来这个答案不那么起眼,但它确实让人们领教了 lambda 演算在抽象层次上的嚣张。同时,E 语言还大胆地加入了对象继承的概念——一个对象可以像类继承一样继承其它对象的属性——因为这仅仅是 lambda 算子的扩展罢了!
但是,无类语言的进步不会就此结束,还有更多、跟有力量的无类语言产生,比如 Scala,一个给无类语言添加了 ML 中的静态多态类型判定和惰性运算的变态……
历史就是这样一个聋子,听不到你的嗟呀。不过,但愿后人会从这些嗟呀中借鉴到很多。

注:还有两个很牛语言没提到:Ruby 和 Python。其中前者学 Smalltalk 学地精神分裂,去掉了 Smalltalk 的强大 IDE 之后居然一炮走红;Python 是个不知道该站在那一边的语言,既没有 3P 又知道逃逸变量或 block 是什么,惨兮兮的。

June 8, 2007

Haskell:函数 or 数据

haskell里面是不是所有的函数都是lambda实现的?不然为什么 :t 操作符所返回的都是 lambda的表示,也就是说都是用lambda解释的.
呵呵,最好讲下haskell里的type.和java或c里面的不同.

是。只是语法上不太像。也可以说不是,因为那是lambda的"1.5版"(我的“术语”,Scheme中的是1.0版,学名“一阶lambda演算”) ——Curry化算子。Curry化算子认为,一切都是函数(包括普通数据),但如果这个数据的产生式中每一项都是严格的(比如 num = 4 + 3, 3、4都是可以直接推导而不需要进行惰性化的),那么这个数据可以免除其作为函数的义务。对于产生式中有需要推导的数据,Haskell仍将其表示为普通 数据(因为类型可以直接推导来确定。只要产生它的表达式中函数调用存在,那么就会被认作函数;但是它的类型只需要一步即可推导,所以在Haskell中,它“看上去”是普通数据。)。
fact 0 = 1
fact n = n * fact (n-1) -- 这是函数,要推导
num = 5 + fact 4 -- 这个是事实上是函数,但类型上看不出来
另外,之所以haskell中的函数全是lambda的表示,和它的类型系统也有关系。前面说了,Haskell中对函数的理解已经超越了数学映 射1.5倍,重点在于对参数列表的理解。数学映射和一阶lambda都认为参数是连续的、一次传递一批,就像Scheme中那样:
(function arg1 arg2)
但Curry化算子认为参数是不连续的、可以分段传递(前提是个数事先固定),每当传递结束而参数不够是,自动转换为一个期待余下参数的函数。这就意味着,如果用Scheme的语法来说明Curry化算子就成了这样:
((+ 1) 2) ;调用一个固定参数个数为两个的函数
这句代码都将行得通!
(map (+ 1) '(1 4 2 5 2))
这对于Scheme来说是非常荒唐的(用宏另当别论),但对于Haskell这是基础:
map (+1) [1, 4, 2, 5, 2]
所以,Haskell的类型系统中对于函数类型的表示才会是那么多个箭头:因为,只要需要推导,就有函数:
Main> :t (+1)
flip (+) 1 :: Num a => a -> a
看出来了吗?不连续的类型导出过程,和java/c等等等等最大的不同点。
至于Haskell类型系统的核心力量——静态多态类型推断,讲起来是在很麻烦。去看中文版的ML语言的书里讲的很详细。

OOP 诡异教程(上)

  • 本文分上、下两篇,站在一个难以名状的角度上研究了 JavaScript 语言中面向对象机制的起源、内涵和发展,带领读者从原始森林走向高楼大厦。文章作者 lichray 是个 ECMAScript 的狂热追随者,mozilla.org 邮件列表里的无名潜水员。
  • 文章中使用了 Rhino 解释器,行开头有 "js>" 表示那是输入,输入下一行没有这个标记的表示解释器回馈消息。
  • PS: 读懂本文需要对 JavaScript 闭包和逃逸变量有较深入的了解。

一. 对象和消息
考虑一下我们平常怎么说话的。我们叫某某人做某事,用下面的句式:
forest run!
其中"!"是语气的标志,对于编程语言来说是没有意义的,全部换成".":
forrest run.
不知道如果我告诉大家上面这句话就是 Smalltalk 语言中一个合法语句大家会怎么想。好了,不谈这个。这样我们就得到了一种语法,"宾"谓结构:
ObjectVerb ::
  Object Verb.
如果让它支持多个 Verb,比如
forrest run, jump, stop.
可以扩展成这样:
ObjectVerb ::
  Object VerbList.
VerbList ::
  Verb
  Verb , VerbList
很明显,对于 JavaScript 来说,上面的 BNF 不可能和任何一个产生式匹配。问题出在哪儿?我们要帮 JavaScript 指定,谁是 Object,谁是 Verb。鉴于 Object 只有一个,Verb 有多个,我们可以用括号来区分它们,然后把最后那个句号去掉:
ObjectVerb ::
  Object ( VerbList )
这样上面的那句话就变成了下面的形式:
forrest (run, jump, stop)
很像函数调用,是吧?不过还有一个问题,现在这些 Verb(s) 对于 JavaScript 来说是“裸词”(Perl 语),我们可以避开再去定义这些标识符,用字符串代替;最后再说明一下 Object 是什么:
forrest ('run', 'jump', 'stop')
那么现在我们第一个“模仿”自然语言的程序版本出现了,加上下面针对 JavaScript 的文法:
Object ::
  Identifier
Verb ::
  StringLiteral

二. 实现消息传递
有了文法,一切都好办。看得出来,我们下面的工作是定义能创建一个新 Object 的函数,函数中有一些动作,产生的新 Object 是一个能处理这些消息的函数。创建 Forrest Gump 的函数还可以创建 Tom,Mike 等等;他们都是 People:
function People () {
  function run () {
    print("I'm running!")
  }
  function jump () {
    print("I'm jumping!")
  }
  function stop () {
    print("I can't stop!")
  }  
  return (function (verb) {
    switch (verb) {
      case 'run': run(); break
      case 'jump': jump() ;break
      case 'stop': stop() ;break
    }
  })
}
为了简单起见还可以把返回的那个函数写成这样:
    (function (verb) {
      eval(verb)();
    }
  })
Ok。现在我们来试一试这个智商低于 85 的 Forrest Gump 怎么样:
js> forrest = People()
js> forrest('run')
I'm running!
js> forrest('jump')
I'm jumping!
js> forrest('stop')
I can't stop!
事情就是这样。我们成功地创造了对象,还让他做动作、说话。
不过,这个实现并不是我们上文中最后一个文法所指出的。它不支持连续发送指令。改一改。要加入顺序执行指令的办法:
function People () {
  function run () {
    print("I'm running!")
  }
  function jump () {
    print("I'm jumping!")
  }
  function stop () {
    print("I can't stop!")
  }
  function _do_verbs_ (verblist) {
    for (var i=0; i <> forrest = People()
js> forrest('jump','run','jump','stop')
I'm jumping!
I'm running!
I'm jumping!
I can't stop!

三. 利用消息传递处理状态
什么是状态?我们在进行面向对象编程时,把状态表示为对象的一组数据,我们称之为“属性(property)”。在我们的消息传递编程风格中,可以直接把这些数据堆到产生对象的那个函数中去。下面给 Forrest 加入一个状态,Forrest 口袋里的钱。先得声明原先有多少钱:
forrest = People(1000)
然后,我们希望可以执行这样的代码,让 forrest 支出 200 美元:
forrest('pay', 200)
但很明显,我们无法分清 200 是 Verb 还是 'pay' 所要求的数据。我们只得简化文法,只允许一次发送一个消息,以保全我们的脑细胞:
forrest('pay')(200)
也就是说,我们需要让 forrest('pay') 这一表达式返回一个能改变状态的函数,而不仅仅是调用函数来显示一句话。也就是说,如果我们想让 Forrest 急得跳起来,我们先得跳起来:
forrest('jump')()
新时代的 Forrest 实现如下(省略了一点多余的代码):
function People (money) {
  //var money = money
  function pay (dollars) {
    money -= dollars
  }
  function restMoney () {
    return money
  }
  function run () {
    print("I'm running!")
  }
  return (function (verb) {
    return eval(verb)
  })
}
试一下。先支出 200 美元,然后看看他还剩多少钱:
js> forrest=People(1000)
js> forrest('restMoney')()
1000
js> forrest('pay')(200)
js> forrest('restMoney')()
800
当然,我们的 Forrest 还可以赚钱。下面这个版本比较彻底地说明了消息传递编程风格的一切。可以直接修改钱之后,我们可以不需要在创建 Object 的时候就说明原有多少钱;当然,使用注释中的版本更自然:
function People (/* money */) {
  var money = 0; // var money = money ? money : 0;
  function setMoney (dollars) {
    money = dollars
  }
  function addMoney (dollars) {
    money += dollars
  }
  function pay (dollars) {
    money -= dollars
  }
  function restMoney () {
    return money
  }
  return (function (verb) {
    return eval(verb)
  })
}
试一下吧:
js> forrest = People()
js> forrest('addMoney')(1000)
js> forrest('restMoney')()
1000
js> forrest('pay')(200)
js> forrest('restMoney')()
800
上篇完。小结一下:消息传递的编程风格指的是,把函数 A 的执行上下文当作对象的数据环境,在此定义对象的动词(函数),然后从此上下文中返回一个可以接受、处理消息的函数(常为匿名)。用函数 A 产生消息处理器作为对象,向此对象传递参数作为消息,以此执行函数 A 环境中定义的动作,这些动作还可能改变所在上下文中用一组数据定义的对象状态。

June 1, 2007

翻译工作转移阵地

ECMA-262的翻译工作开始的翻译对实际编程非常重要的东东(第8章之后),需要更多协作,反对我博客变成翻译垃圾堆,所以,今起转移阵地,使用Google文件。
这是 RSS供稿地址。