Nirvana Studio » Scheme :: 分享知识,传播技术

Archive for the 'Scheme' Category

为何 Java(以及很多其他编程语言)令人不爽

Posted by ShiningRay on 26th 七月 2006

作者:Ed Watkeys, edw@xmog.com 翻译:ShiningRay

原文地址:http://xmog.com/scrap/show/5
September 8, 2005
Updated December 27, 2005

Java 5.0其中一个最大的新特性是引入了一种迭代集合的新语法,用来替代以前繁琐的格式(如下):

for (Iterator i = c.iterator(); i.hasNext(); ) {
    String s = (String) i.next();
    ...
}

现在,多亏了这种新的优雅的迭代语法,以及泛型的引入,我们可以用下面的代码:

for (String s : c) {
    ...
}

很明显,可以少输入很多字符。但请考虑一个问题:为什么花了10年才引入了这个特性?不过我们先不考虑这个重要的问题,再看看另一种语言,Python。

Python在很多方面都是一个更加适合编程的语言。它的衍化要比Java快得多。例如,Python 2.2引入了生成器(generator)。一个生成器是一个可以产生多个值得函数,在每次调用时都会保存状态。下面是一个简单的例子:

def counter(n):
  while True:
    yield n
    n = n + 1

因为这个函数定义包含了关键词yield,所以Python就可以知道它是一个生成器。可以像下面这样使用counter生成器:

c12 = counter(12)
c12.next()
c12.next()

第一行创建了从12开始计数的生成器的实例。第二行告诉生成器运行到产生(yield)一个值为止。第三行告诉生成器继续运行直到产生了另一个值。该生成器所产生的前两个值分别是整数12和13。

这确实是一个很酷的特性:它让程序员能写出更简单的代码,而不会使生成器变得复杂和容易出错。为何Java不能学习Python呢?

我们也先将第二个问题放一边,思考一下如何用另一种语言来实现Python的生成器,这种语言就是Scheme——世界上有一些自以为是的怪人就用它。Scheme是Lisp的一种方言,它从诞生到现在已经存在了大约30年了。Lisp则已经存在了大约50年了。

下面是我可以完成的对Python中counter生成器模仿最好的Scheme实现:


对资深Lisp程序员多说一句:下面我要演示的子程序要比官方的累加器例子复杂得多,这是因为我是按照了Python生成器的语义来写的。


(define (counter n)
  (letrec ((generator
            (lambda (yield)
              (let counter ((n n))
                (call-with-current-continuation
                 (lambda (continue)
                   (set! generator (lambda (k)
                                     (set! yield k)
                                     (continue n)))
                   (yield n)))
                (counter (+ n 1))))))
    (lambda () (call-with-current-continuation
                (lambda (yield)
                  (generator yield))))))

“我靠!”你可能会有这种反应。确实太复杂了!在写这段代码的最初版本的时候,我说写这个不会太难。然后我发现了一个可能导致死循环的错误,而引发错误不是小概率事件。所以最后我认同了这点:如果只是要写一个能和Python生成器效果一样的函数,还是不要写这样的子过程的比较好。不过,这个可怕的东西在客户端代码使用起来却十分简单:

(define c12 (counter 12))
(c12)
(c12)

第一行定义了c12是子过程counter给一个参数12调用时的结果。第二行和第三行直接调用c12,没有任何参数,就和Python的例子一样,返回了12和13。不过这些都是学院派的,没有哪个疯子会在普通需求下写一个这样的子过程。

写像counter这样的子过程一般会导致手指抽痉、头脑发胀。不过,有意思的是,我们可以跳过这些来写counter,Scheme的生成器要比Python版本的更加容易使用,因为Scheme的返回的是函数,而Python生成器返回的是生成器对象,所以Python生成器需要调用next方法。

(旁白:Python生成器的设计师们本可以这样实现生成器对象:接下来的值通过c12()c12.next()来获取,不过他们并没有这样实现。)

回到Scheme上……在Scheme中写这样的生成器的复杂和容易出错看上去似乎让在Scheme中使用生成器变得不切实际,但实际上并非如此,因为Scheme包含了一个Python和Java都缺乏的特性:扩展语言语法的能力。如果你能够写出Scheme版本的counter,花不了多少功夫就可以创建一个宏(macro)使得这个特性能以一种可以被大家接受的方式使用。下面是我写的宏,可以完成这个任务:

(define-syntax define-generator
  (syntax-rules ()
    ((define-generator (NAME ARG ...) YIELD-PROC E1 E2 ...)
     (define (NAME ARG ...)
       (letrec ((generator
                 (lambda (yield)
                   (let ((YIELD-PROC
                          (lambda v
                            (call-with-current-continuation
                             (lambda (continue)
                               (set! generator (lambda (k)
                                                 (set! yield k)
                                                 (apply continue v)))
                               (apply yield v))))))
                     (let NAME ((ARG ARG) ...)
                       E1 E2 ...)))))
         (lambda () (call-with-current-continuation
                     (lambda (yield)
                       (generator yield)))))))))

一旦有了这个宏,counter生成器的Scheme版本就可以这样定义了:

(define-generator (counter n) yield
  (counter (+ 1 (yield n))))

还不错吧?这个版本唯一让我烦的地方是必须指定yield函数的名称。不过它还是给予程序员一些灵活性,可以根据代码的上下文来给函数起一个最有意义的名称。(其实,资深的Lisp程序员应该知道这个“特性”可以使用一些非hygenic宏来修正,不过这里我们还是坚持标准R5RS Scheme)。

如果你比较一下第一版和第二版的counter,你可能会注意到我在新的define-generator版本中作了一些小手脚:yield函数返回了它产生的值,因此它可以用于对counter的递归调用中。而Python的生成器就不能这样用。

那么为什么Java不能变得更像Python?答案是——其实Java和Python很像:Python的用户也等了将近10年才可以用上生成器。而我在玩了几天之后花了几个小时就在Scheme中加入了对生成器的支持。不过还是有人会说生成器以及最近的其他一些Python特性,如列表包容,都使得Python变得更加容易编写——我当然完全同意这个观点——但是,从根本上来说,Java和Python在这一点上是一样的——都不能修改语言本身。

Java、Python和几乎所有其他非Lisp语言都是让你任由语言设计者摆布。你要等他们实现你需要的语言特性,可能只有列表中的前几个。而且等他们弄个一个新东西给你搔搔痒,谁能确定你喜欢这种结果?

那么为何花了10年才在Java中有了增强迭代语法?这是因为在Java和很多其他编程语言一样,语法是一个大问题。一般用户都不会修改语言的语言。因为这很难完成,只有少数人学习了这种技术才能去做。当需要修改语法时,表达力和清晰的优先级都没有保持向后兼容重要。

在Scheme中,加入语法相对还比较简单,同时还可以根据特定问题的基础来完成,所以无须担心是有要给出一个普遍适用的理想解决方案。这种能根据问题构建语言的能力要胜过对使用很多括号的语言的担心。

链接

Posted in Java, Lisp, Python, Scheme | 7 Comments »