返回介绍

附录:编程能力

发布于 2023-05-23 15:33:22 字数 3755 浏览 0 评论 0 收藏 0

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

Common Lisp^的写法如下:

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

^「下面是一些Lisp方言生成累加器函数的写法:

Scheme:

(define (foo n)
  (lambda (i) (set! n (+ n i)) n))

Goo: (df foo (n) (op incf n _)))

Arc: (def foo (n) [++ n _])」

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,你会遇到一些限制。因为Pythcn并不完全支持词法变量,你不得不创造一种数据结构来接受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)中的所有变量。这有点类似于让程序员自己做代码分析,本来这应该是由支持词法作用域(lexical 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,看上去使用它们根本无法解决这个问题。肯·安德森说,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),也就是人肉编译器^。当我在自己的程序中发现用到了模式,我觉得这就表明某个地方出错了。程序的形式应该仅仅反映它所要解决的问题。代码中其他任何外加的形式都是一个信号,(至少对我来说)表明我对问题的抽象还不够深,也经常提醒我,自己正在手工完成的事情,本应该写代码通过宏的扩展自动实现。

「皮特·诺维格发现,总共23种设计模式之中,有16种在Lisp语言中“本身就提供,或者被大大简化”。(www.norvig.com/design-pattems)」

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文