程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 探索JVM上的LISP

探索JVM上的LISP

編輯:關於JAVA

當前Java領域最激動人心的事情莫過於可允許其它編程語言運行於Java虛擬機上。圍繞JRuby、Groovy、Scala還有 Rhino(JavaScript引擎)的討論已經甚囂塵上。可為什麼要墨守陳規呢?如果你真的想跳出主流,投身於一種與Java截然不同的的語言,Lisp就不失為一種很好的選擇。現在已有幾種可運行於JVM上的Lisp程序設計語言的開源實現,准備好開始我們的探索之旅吧!

Lisp有什麼值得研究呢?首先,作為已有50年歷史的語言,它促成許多被我們今日視為理所當然的觀念。if-then-else結構、早期的面向對象和帶垃圾回收的自動內存管理的嘗試都來源於此。目前Java程序員的熱點話題——詞匯閉包(Lexical Closure),最初的探索也是七十年代在Lisp中展開的。除此以外,Lisp還具備其它許多語言至今都未采用的特性,這些出色的思想必將在未來引起復興潮流。

本文的目標讀者是有意了解Lisp的Java開發人員。我們將在接下來的內容中討論當前可以用在JVM上的不同Lisp方言(dialect),令你快速了解Lisp程序設計工作機理和其獨特之處,文章的最後會演示如何將Lisp代碼與Java系統進行整合。

目前存在許多可用於不同平台的Lisp系統,有免費的也有商業的。對於想要開始探索Lisp的Java用戶,不離開JVM是首選,這樣的話起步很容易,還可以很方便的使用所有自己熟悉的Java庫和相關工具。

Common Lisp和Scheme

Lisp有兩種主要方言(dialect):Common Lisp和Scheme。雖然設計理念大體相似,但是它們的差別仍然足夠引起孰優孰劣的激烈爭論。

Common Lisp是1991年完成的ANSI標准。統一了幾種早期Lisp的理念,是可用於多種應用開發的大型環境,其最為著名的應用是人工智能。而Scheme 產生於學術界,特意進行了精簡化設計,經驗證是一種很好的語言,既可用於計算機科學教學,又可以作為嵌入式腳本語言。你還可能會遇到其它一些比較有名的 Lisp:小型的特定於應用的DSLs,如Emacs Lisp或AutoCAD的AutoLISP。

上面提到的兩種主要方言(dialect)在JVM上都有相應的實現,相較而言Schemes的實現要成熟一些。Armed Bear Common Lisp(www.armedbear.org/abcl.html)非常徹底的實現了Common Lisp標准,但它存在一個問題,如果你沒有安裝別的Common List系統,就不能構建分發版本,這對新手可能是個困難。

在Scheme方面,兩個主要的產品是Kawa(www.gnu.org/software/kawa)和SISC(www.sisc-scheme.org——the Second Interpreter of Scheme Code)。在這篇文章的例子當中,我們會用到Kawa,它實際上是個框架,能創造可編譯成Java字節碼的新語言。Scheme只是它的實現之一。順便說一句,Kawa的創建者Per Bothner目前就職於Sun,主要從事JavaFX項目的編譯器方面的工作。

另外一個值得一提的竟爭對手是Clojure(clojure.sourceforge.net)。這是一種新的語言,其Lisp方言(dialect)介於Scheme和Common Lisp之間。它是直接為JVM量身打造的,因此在上面提到的所有Lisp當中,有著最為清晰Java整合方案。它還具有其它一些激動人心的特性,例如內建的支持並發和事務內存。Clojure目前仍然處於探索測試階段,因此在它基礎上構建程序還有些為時尚早,但它絕對是一個值得關注的項目。

讀取—求值—打印—循環

我們先來安裝Kawa。它的分發版是一個單獨的Jar文件,可以直接通過鏈接ftp://ftp.gnu.org/pub/gnu/kawa/kawa-1.9.1.jar下載。得到該Jar包後,就把它加進你的類路徑上,這樣你就可以通過運行如下命令啟動REPL了:

java kawa.repl
  #|kawa:1|#

該命令啟動了Kawa,並顯示一個提示符。這其中究竟有何奧妙呢?REPL(READ-EVAL-PRINT-LOOP)意思是讀取—求值—打印—循環,這是與運行中的Lisp系統進行交互的方式——它“讀取”你的輸入,進行“求值”運算後,“打印”計算結果,如此反復“循環”。開發Lisp程序的方式,與我們開發Java程序時所遵循的“寫代碼、編譯、運行”的周期不同。Lisp程序員需要激勵他們的Lisp系統,保持它的運行狀態,這樣就令編譯和運行時的界限模糊起來。在REPL中,函數和變量在執行過程中都是可以修改的,代碼也是動態解釋和編譯的。

先來做點簡單的事情:把兩個數字加到一起。

#|kawa:1|# (+ 1 2)
  3

這是Lisp表達式的典型結構或者說“格式”。語法都是一致的:表達式總被放在一對圓括號內,因為用的是前綴符號,所以“+”號要放在兩個參量前。再來一個復雜點的結構,把幾個格式嵌套在一起,建立一個樹狀結構:

#|kawa:2|# (* (+ 1 2) (- 3 4))
  -3

Scheme的內建函數以同種機理工作:

#|kawa:3|# (if (> (string-length "Hello world") 5)
          (display "Longer than 5 characters"))
  Longer than 5 characters

上面程序中,用一個if語句來檢查某一特定字符串的長度是否超過5個字符,如果像例子中的那樣檢查結果為真,就會執行緊隨其後的表達式,該語句將會打印一條提示信息。注意這裡的縮進只是為了增加可讀性,如果你願意的話,可以在一行內寫下所有的語句。

Lisp代碼用的這種括號密集(parenthesis-heavy)的風格也稱為“S表達式(s-expressions)”。它可兼作定義結構化數據的通用方法,就像XML一樣。Lisp有很多內建的函數,你可以很方便的應用S表達式格式操縱數據,這種便利轉而促成Lisp的另外一個強大優勢:既然語法是如此簡單,那麼編寫產生、修改代碼的程序也要比其它語言簡單得多。當我們演示宏(macros)的例子時,會了解到更多類似情況。

函數

Scheme通常被看做是函數式程序設計語言大家庭中的一員。與面向對象領域不同,Scheme抽象的主要手段是函數和它操縱的數據,而不是類和對象。在這裡,你所做的每一件事,實際上都是調用一些帶有參數、能夠返回運行結果的函數。你可以通過define關鍵字來創建函數:

#|kawa:4|# (define (add a b) (+ a b))

以上代碼定義了一個add函數,它接收a和b兩個參數。函數體簡單地執行加法(+)計算後自動返回執行結果。注意這裡沒有靜態的類型聲明,所有的類型檢查都在運行時進行,這同其它動態語言中的方式並無二致。

定義了上面函數後,你可以很簡單的在REPL中調用它:

#|kawa:5|# (add 1 2)  3

在Scheme的世界裡,函數是一等公民,它可以像Java中的對象一樣被傳遞,這開啟了一些非常有趣的可能性。下面我們將創建一個函數,它接收一個參數,並使它的值增加一倍:

#|kawa:6|# (define (double a) (* a 2))

然後通過調用list函數定義一個包含三個數字的列表:

#|kawa:7|# (define numbers (list 1 2 3))

下面是最令人興奮的部分:

#|kawa:8|# (map double numbers)  (2 4 6)

此處調用了帶有兩個參數的map函數:一個參數是個函數,另外一個參數是個列表(list)。map函數將遍歷列表中的每個元素,將其作為參數調用所提供的函數,最後將所得結果組成一個新列表(list),正如我們在REPL中所看到的。這是可以實現Java中for循環功能的更加函數化的方法。

LAMBDAS

還有一個比較方便的地方在於可以利用lambda關鍵字定義匿名函數,這與Java匿名內部類工作機制類似。重新寫上面的例程,跳過中間定義double函數那一段,map語句可寫成如下形式:

#|kawa:9|# (map (lambda (a) (* 2 a)) numbers)
  (2 4 6)

定義僅返回lambda的函數也是有可能的,經典教科書中的例程會這樣寫:

#|kawa:10|# (define (make-adder a) (lambda (b) (+ a b)))
  #|kawa:11|# (make-adder 2)
  #

上面的語句都做些什麼事情呢?首先定義了一個名為make-adder函數,它帶有一個參數a,返回一個匿名函數,該匿名函數要接收另外一個參數b。當調用發生時,匿名函數會計算a與b的和。

執行(make-adder 2)——或者通俗的說“給我一個函數,可以把2加到傳給它的參數上”,REPL將顯示一些代碼,它實際上是把lambda過程作為一個字符串打印出來,要用這個函數你還可以這樣寫:

#|kawa:12|# (define add-3 (make-adder 3))
  #|kawa:13|# (add-3 2)
  5

此處最為重要的事情在於lambda作為一個閉包執行。把它“封裝”起來,保持它被創建時對作用范圍內變量的引用。(make-adder 3)調用後,作為返回結果的lambda保有a的值,當(add-3 2)執行時,它計算3+2的值,並返回預期的5。

宏(MACROS)

到目前為止所看到的特性都和我們在比較新的動態語言中發現的相類似,例如Ruby,它也允許你使用匿名塊處理對象收集,正如我們在前面用lambda和map函數所做的一樣。所以,現在讓我們來轉變一下方向,看看獨屬於Lisp的特性:宏(macros)。

Scheme和Common Lisp都有宏系統。人們在提到Lisp時總說它是“可編程的程序設計語言”,其實指得就是這個。有了宏,你實際上就可以和編譯器建立關聯,重新定義語言本身。此時Lisp統一的語法才真正開始揮效用,所有的事情都變得有趣起來。

舉個簡單的例子,我們可以看一下循環。在Scheme語言中,最初並沒有定義循環,典型的對某集合進行迭代的方式是使用map或者遞歸函數調用。多虧有一個編譯器小竅門——尾調用優化遞歸(tail-call optimizations recursion)——可以采用而不必擔心會擠爆棧。下面將介紹一個非常靈活的do命令並應用它來執行一個循環,實現的程序如下:

(do ((i 0 (+ i 1)))
   ((= i 5) #t)
   (display "Print this "))

上面程序中定義了一個索引變量i,初始化為0,設置按照增量1迭代增長。當表達式(= i 5)的值為真時,循環中止,返回#t(它和Java中的布爾值true相當)。在循環裡我們只是打印了一個字符串。

如果我們所需要做的只是一個簡單的循環,上面這個例子就有很多冗余的公式化代碼了。在很多情況下更可取的應當是簡單直接的實現方式:

(dotimes 5 (display "Print this"))

多虧了宏(macros),才有可能適當地使用稱為define-syntax函數,把關於dotimes的特殊語法添加進語言:

(define-syntax dotimes
  (syntax-rules ()
   ((dotimes count command) ; Pattern to match
    (do ((i 0 (+ i 1)))   ; What it expands to
      ((= i count) #t)
      command))))

執行上述命令可以告訴系統,任何對dotimes的調用都要被特別對待。Scheme將用我們定義的語法規則匹配一個模式,並在將結果送到編譯器之前將其展開。在這個例子中,模式是(dotimes count command),它被轉換為標准的do循環。

在REPL中執行該語句,你會得到如下結果:

#|kawa:14|# (dotimes 5 (display "Print this "))
Print this Print this Print this Print this Print this #t

上述例子之後必然產生兩個問題。第一,為什麼我們需要使用宏(macro)?用一個常規的函數不能做這些事情麼?答案是“不可以”。任何對函數的調用實際上在開始之前都會觸發對它所有參數的求值操作,在上面的例子中就不會發生這種情況。比方說,你怎樣處理(do-times 0 (format #t "Never print this"))呢?當求值需要被延遲時,只有宏(macro)才能完成這個功能。

其次,我們在宏裡用了變量i,如果在command表達式中碰巧有一個變量取相同的名字,這會不會產生沖突呢?這點不必擔心,Scheme的宏以“衛生”著稱。編譯器會自動檢測並熟知如何處理這樣的命名沖突,對程序員是完全透明的。

了解到這些情況後,試想一下在Java中添加你自己的循環結構,這近乎不可能。也可以說,不是非常可能,畢竟編譯器是開源的,所以你可以自由下載並恰當使用,但這真的是一個不太現實的選擇。在其它動態語言中,閉包可以給你多些自由,對語言按照自己的習慣做些改動,但是仍然存在這種情況:他們的結構並沒有足夠靈活和強大到可以讓你自由調整語法的程度。

這種能力就是為什麼每當元編程語言或特定領域語言被提及時,Lisp總是以勝利者姿態出現的原因。Lisp程序員長期以來一直是徹頭徹尾的“自底向上編程(bottom-up programming)”的冠軍,因為當語言本身已經被調節為適合你的問題領域時,障礙會少許多。

在Java中調用Scheme代碼

將別的語言運行在JVM之上的一個主要好處是,不管代碼用何種語言寫成,都可與現存的應用進行整合。因此很容易想象,可以用Scheme來模型化一些復雜的具有易變趨勢的業務邏輯,然後將它嵌入一個比較穩定的Java框架中。規則引擎Jess(www.jessrules.com)是一個很好的范例,它運行於JVM之上,但是用自己的類Lisp語言來聲明規則。

但是讓不同的程序設計語言以一種界限清晰的方式協同工作還是一個棘手的問題,尤其是像Java和Lisp這樣存在天壤之別的語言。如何做這種整合並沒有標准,所有活躍在JVM上的方言都以不同的方式處理著問題。Kawa對於Java整合的支持相對較好,所以在下面的例子中,我們將繼續用它來研究怎樣用 Scheme代碼來定義一個Swing GUI。

在Java程序中運行Kawa代碼是很簡單的:

import java.io.InputStream;
import java.io.InputStreamReader;
import kawa.standard.Scheme;
public class SwingExample {
   public void run() {
     InputStream is = getClass().getResourceAsStream("/swing-app.scm");
     InputStreamReader isr = new InputStreamReader(is);
     Scheme scm = new Scheme();
     try {
       scm.eval(isr);
     } catch (Throwable schemeException) {
       schemeException.printStackTrace();
     }
   }
   public static void main(String arg[]) {
     new SwingExample().run();
   }
}

在這個例子中,首先會在類路徑上尋找包含Scheme程序的叫做swing-app.scm的文件,然後創建解釋程序kawa.standard.Scheme的實例,調用它來解釋文件中內容。

Kawa還不支持在Java 1.6中引入的JSR-223規定的腳本APIs(javax.scripting.ScriptEngine等),如果你需要能做這種事情的Lisp,最好的選擇應該是SISC。

在Scheme中調用Java庫

在我們開始寫大型Lisp程序之前,是時候找個比較合適的編輯器了,否則光是驗證括號匹配的工作就夠讓人發瘋了。最受歡迎的選擇之一肯定是Emacs,畢竟它可用自己的Lisp方言進行編程,不過對於Java開發者繼續使用Eclipse可能更舒服些。如果你是這種情況就需要在工作開始之前先安裝一個免費的SchemeScript插件。你可以在這個網站找到它。這裡還有一個稱為Cusp的插件,可以用於Common Lisp的開發。

現在,我們可以來看一下swing-app.scm的具體內容,以及用Kawa定義一個簡單的GUI都需要做什麼樣的工作。這個例子將會打開一個帶有按鈕(button)的frame,按鈕點擊一次後它就會被禁用。

(define-namespace JFrame )
(define-namespace JButton )
(define-namespace ActionListener )
(define-namespace ActionEvent )
(define frame (make JFrame))
(define button (make JButton "Click only once"))
(define action-listener
  (object (ActionListener)
   ((action-performed e :: ActionEvent) ::
    (*:set-enabled button #f))))
(*:set-default-close-operation frame (JFrame:.EXIT_ON_CLOSE))
(*:add-action-listener button action-listener)
(*:add frame button)
(*:pack frame)
(*:set-visible frame #t)

最初幾行用define-namespace命令為將要用到的Java類定義縮略名,這同Java的import聲明功能類似。

然後定義了frame和button,利用make函數可以創建Java對象。創建button時,我們提供一個字符串作為參數傳給構造函數,Kawa可以很智能的將它翻譯成需要的java.lang.String對象。

現在讓我們跳過ActionListener的定義,先來看一下最後5行代碼。這裡的符號*:用於觸發對象中的方法。例如,(*:add frame button)的功能就等同於frame.add(button)。你要注意到Scheme特有的,可以自動將方法名從Java中的駱駝拼寫風格轉換為小寫的以連字符分隔單詞。例如,set-default-close-operation將被轉換為setDefaultCloseOperation。這裡另外一個細節是:.可被用來訪問靜態域,(JFrame:.EXIT_ON_CLOSE)等同於JFrame.EXIT_ON_CLOSE。

現在來回頭看一下ActionListener。這裡用object函數創建了一個實現了java.awt.event.ActionListener接口的匿名類,action-performed函數被用來調用button上的setEnabled(false)方法。此時還需要添加些信息可以讓編譯器知道action-performed是ActionListener接口中定義的void actionPerformed(ActionEvent e)的實現。早先我們曾經說過,正常情況下在Scheme中並不需要類型,但是此時,當與Java協同工作時,編譯器就需要多知道一些信息。

當你有了這兩個文件後,編譯SwingExample.java,並且確認將編譯後的類和swing-app.scm文件放到類路徑上,接下來就可運行java SwingExample來看看GUI的效果。你同樣也可以用load函數: (load "swing-app.scm")在REPL中執行文件中的代碼,這開啟了動態操縱GUI構件的先河。例如,你可以通過在REPL中執行(*:set-text button "New text")來快速更改button上的文字,而且可以立即看到修改結果生效。

當然,這個例子只是想簡單的演示如何從Kawa中調用Java,無論如何它都不是你能想象中的最優質的Scheme代碼。如果你確實想要在Scheme中定義一個大型Swing UI,那你最好提升一點抽象級別,用一些精選的函數和宏來隱藏凌亂的整合代碼。

關於作者

Per Jacobsson是位於洛杉矶的eHarmony.com的軟件架構師,應用Java已有10年歷史,近兩年成為Lisp的狂熱愛好者。你可以通過pjacobsson.com與他取得聯系。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved