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):
whileTrue:
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 on 2007-01-12 20:28
???MengChuChen 閱讀(166)
評論(0) 編輯 收藏