【节选】语言编程能力

本文节选自《黑客与画家》
作者:Paul Graham
译者:阮一峰

—————————————–分割线——————————————————-

附录:编程能力

  为了解释我所说的语言编程能力不一样,请考虑下面的问题。我们需要写一个函数,它能够生成累加器,即这个函数接受一个参数n,然后返回另一个函数,后者接受参数i,然后返回n增加(increment)了i后的值。

  Common Lisp的写法如下:

      (defun foo (n)
        (lambda (i) (incf n i)))

  Ruby的写法几乎完全相同:

      def foo (n)
        lambda {|i| n += i } end

  Perl 5的写法则是:

      sub foo {
        my ($n) = @_;
        sub {$n += shift}
      }

  这比Lisp和Ruby的版本,有更多的语法元素,因为在Perl语言中,你不得不手工提取参数。

  Smalltalk的写法稍微比Lisp和Ruby的长一点:

      foo: n
        |s|
        s := n.
        ^[:i| s := s+i. ]

  因为在Smalltalk中,局部变量(lexical variable)是有效的,但是你无法给一个参数赋值,因此不得不设置了一个新变量,接受累加后的值。

  Javascript的写法也比Lisp和Ruby稍微长一点,因为Javascript依然区分语句和表达式,所以你需要明确指定return语句,来返回一个值:

      function foo (n) {
        return function (i) {
          return n += i } }

  (实事求是地说,Perl也保留了语句和表达式的区别,但是使用了典型的Perl方式处理,使你可以省略return。)

  如果想把Lisp/Ruby/Perl/Smalltalk/Javascript的版本改成Python,你会遇到一些限制。因为Python并不完全支持局部变量,你不得不创造一种数据结构,来接受n的值。而且尽管Python确实支持函数数据类型,但是没有一种字面量的表示方式(literal representation)可以生成函数(除非函数体只有一个表达式),所以你需要创造一个命名函数,把它返回。最后的写法如下:

      def foo (n):
        s = [n]
        def bar (i):
          s[0] += i
          return s[0]
        return bar

  Python用户完全可以合理地质疑,为什么不能写成下面这样:

      def foo (n):
        return lambda i: return n += i

  或者:

      def foo (n):
        lambda i: n += i

  我猜想,Python有一天会支持这样的写法。(如果你不想等到Python慢慢进化到更像Lisp,你总是可以直接……)

  在面向对象编程的语言中,你能够在有限程度上模拟一个闭包(即一个函数,通过它可以引用由包含这个函数的代码所定义的变量)。你定义一个类(class),里面有一个方法和一个属性,用于替换封闭作用域(enclosing scope)中的所有变量。这有点类似于让程序员自己做代码分析,本来这应该是由支持局部作用域的编译器完成的。如果有多个函数,同时指向相同的变量,那么这种方法就会失效,但是在这个简单的例子中,它已经足够了。

  Python高手看来也同意,这是解决这个问题的比较好的方法,写法如下:

      def foo (n):
        class acc:
          def _ _init_ _ (self, s):
            self.s = s
          def inc (self, i):
            self.s += i
            return self.s
        return acc (n).inc

  或者

      class foo:
        def _ _init_ _ (self, n):
          self.n = n
        def _ _call_ _ (self, i):
          self.n += i
          return self.n

  我添加这一段,原因是想避免Python爱好者说我误解这种语言。但是,在我看来,这两种写法好像都比第一个版本更复杂。你实际上就是在做同样的事,只不过划出了一个独立的区域,保存累加器函数,区别只是保存在对象的一个属性中,而不是保存在列表(list)的头(head)中。使用这些特殊的内部属性名(尤其是call),看上去并不像常规的解法,更像是一种破解。

  在Perl和Python的较量中,Python黑客的观点似乎是认为Python比Perl更优雅,但是这个例子表明,最终来说,编程能力决定了优雅。Perl的写法更简单(包含更少的语法元素),尽管它的语法有一点丑陋。

  其他语言怎么样?前文曾经提到过Fortran、C、C++、Java和Visual Basic,看上去使用它们,根本无法解决这个问题。Ken Anderson说,Java只能写出一个近似的解法:

      public interface Inttoint {
        public int call (int i);
      }

      public static Inttoint foo (final int n) {
        return new Inttoint () {
        int s = n;
        public int call (int i) {
        s = s + i;
        return s;
        }};
      }

  这种写法不符合题目要求,因为它只对整数有效。

  当然,我说使用其他语言无法解决这个问题,这句话并不完全正确。所有这些语言都是图灵等价的,这意味着严格地说,你能使用它们之中的任何一种语言,写出任何一个程序。那么,怎样才能做到这一点呢?就这个小小的例子而言,你可以使用这些不那么强大的语言,写一个Lisp解释器就行了。

  这样做听上去好像开玩笑,但是在大型编程项目中,却不同程度地广泛存在。因此,有人把它总结出来,起名为”格林斯潘第十定律”(Greenspun’s Tenth Rule):

  “任何C或Fortran程序复杂到一定程度之后,都会包含一个临时开发的、只有一半功能的、不完全符合规格的、到处都是bug的、运行速度很慢的Common Lisp实现。”

  如果你想解决一个困难的问题,关键不是你使用的语言是否强大,而是好几个因素同时发挥作用(a)使用一种强大的语言,(b)为这个难题写一个事实上的解释器,或者(c)你自己变成这个难题的人肉编译器。在Python的例子中,这样的处理方法已经开始出现了,我们实际上就是自己写代码,模拟出编译器实现局部变量的功能。

  这种实践不仅很普遍,而且已经制度化了。举例来说,在面向对象编程的世界中,我们大量听到”模式”(pattern)这个词,我觉得那些”模式”就是现实中的因素(c),也就是人肉编译器。 当我在自己的程序中,发现用到了模式,我觉得这就表明某个地方出错了。程序的形式,应该仅仅反映它所要解决的问题。代码中其他任何外加的形式,都是一个信号,(至少对我来说)表明我对问题的抽象还不够深,也经常提醒我,自己正在手工完成的事情,本应该写代码,通过宏的扩展自动实现。