本文翻譯自:Casting SPELs in Clojure
準備
任何學過Lisp的人都會說Lisp和其它語言有很大的不同.它有很多不可思議的地方.本文將告訴你它有哪些獨特之處!
本文適用于Clojure,它是一個運行在JVM上的Lisp方言.Clojure的API和語法和
Common Lisp很類似,但是還是有足夠多的區(qū)別,需要單獨為其寫個教程.
在大部分情況下,我們會說Lisp而不是Clojure,因為大部分的概念在Lisp中是通用的.我們會指出Clojure特有的內(nèi)容.
Clojure是運行在JVM之上的,所以你需要先安裝JVM.如果你是MAC機,那么Java已經(jīng)被安裝過了.如果是Linux或者Windows系統(tǒng),你需要到Oracle Java官網(wǎng)下載對應版本的Java.
而Clojure,你可以從它的官網(wǎng)獲得最新版本.下載完成后,你只需要解壓縮,打開命令行,切換到解壓縮目錄,輸入:
java -jar clojure.jar
如果沒有問題,那么你將會看到Clojure輸出提示
Clojure 1.6.0
user=>
教程中有很多Clojure代碼片段,類似下面的樣子:
'(these kinds of boxes)
你只需要將這些代碼片段拷貝到Clojure REPL中運行就可以了!當你學習完此教程,你將會有一個你自己的文字冒險游戲了!
語法和語義
每一個編程語言是由語法和語義組成的.語法是組成你的程序的骨架,你必須要遵循它們,這樣編譯器才能知道你的程序里什么是什么,比如說什么是函數(shù),什么是變量,等等!
而語義是個比較"隨便"的東西,例如你的程序里有哪些不同的命令,或者在程序的哪個部分能訪問到哪些變量!這里Lisp比較特別的地方就是,Lisp的語法比其它任何語言都要簡單.
首先,Lisp語法規(guī)定,所有傳遞給Lisp編譯器的文本需要是個list,當然這個list可以無限嵌套.每個list都必須使用括號包裹.
另外,Lisp編譯器使用兩種模式來讀取你的代碼:代碼模式和數(shù)據(jù)模式.當你在數(shù)據(jù)模式下,你可以將任何東西塞到你的list中.但是在代碼模式下,你的list需要是叫做form的特殊類型.
form也是個list,不過它的第一個符號被lisp編譯器特殊對待了---一般被當做函數(shù)的名字.在這種情況下,編譯器會將list中的其它元素作為函數(shù)參數(shù)傳遞給這個函數(shù).默認情況下,編譯器運行在代碼模式下,除非你特意告訴它進入數(shù)據(jù)模式.
為我們的游戲世界定義數(shù)據(jù)
為了進一步的學習form,讓我們來創(chuàng)建一些form,來定義我們游戲世界里的數(shù)據(jù).首先,我們的游戲有一些對象,玩家可以使用他們--讓我們來定義吧:
(def objects '(whiskey-bottle bucket frog chain))
讓我們來看看這行代碼是什么意思:Lisp編譯器總是使用代碼模式來讀取內(nèi)容,所以第一個符號(這里是def),肯定是個命令.
在這里,它的作用就是給某個變量設值:這里變量就是objects,而值是一個包含四個對象的list.這個list是數(shù)據(jù)(我們可不想編譯器去調(diào)用一個叫做whiskey-bottle的函數(shù)),所以在讀取這個list時
我們需要將其設值為數(shù)據(jù)模式.在list前面的哪個單引號就是干這個的:
def命令就是用來設值的(如果你學過Common Lisp,你應該會知道它和CommonLisp中的setf命令等價,但是Clojure中沒有setf命令)
現(xiàn)在我們在游戲里定義了一些對象,現(xiàn)在讓我們來定義一下游戲地圖.下面是我們的游戲世界:
在這個簡單的游戲里,只有三個地點:一個房子,它包含起居室,閣樓和花園.讓我們來定義一個新變量,叫做game-map來描述這個游戲地圖:
(def game-map (hash-map
'living-room '((you are in the living room
of a wizards house - there is a wizard
snoring loudly on the couch -)
(west door garden)
(upstairs stairway attic))
'garden '((you are in a beautiful garden -
there is a well in front of you -)
(east door living-room))
'attic '((you are in the attic of the
wizards house - there is a giant
welding torch in the corner -)
(downstairs stairway living-room))))
這個map包含了三個地點的所有重要信息:每個地點都有個獨立的名字,一個簡短的描述,描述了我們能在這些地點看到什么,以及如何進入此處或從此處出去.
請注意這個包含了豐富信息的變量是如何定義的---Lisp程序員更喜歡用小巧的代碼片段而不是一大片代碼,因為小代碼更容易理解.
現(xiàn)在我們有了一個地圖和一組對象,讓我們來創(chuàng)建另一個變量,來描述這些對象在地圖的哪些地方.
(def object-locations (hash-map
'whiskey-bottle 'living-room
'bucket 'living-room
'chain 'garden
'frog 'garden))
這里我們將每個對象和地點進行了關聯(lián).Clojure提供了Map這個數(shù)據(jù)結構.Map使用hash-map函數(shù)來創(chuàng)建,它需要一組參數(shù)類似(key1 value1 keys value2...).我們的game-map變量也是個Map---三個key分別是living-room,garden和attic.
我們定義了游戲世界,以及游戲世界中的對象,現(xiàn)在就剩下一件事了,就是描述玩家的地點!
(def location 'living-room)
搞定,現(xiàn)在讓我們來定義游戲操作吧!
環(huán)顧我們的游戲世界
我們想要的第一個命令能夠告訴我們當前地點的描述.那么我們該怎么定義這個函數(shù)呢?它要知道我們想要描述的地點以及能夠從map中查找地點的描述.如下:
(defn describe-location [location game-map]
(first (location game-map)))
defn定義了一個函數(shù).函數(shù)的名字叫做describe-location,它需要兩個參數(shù):地點和游戲地圖.這兩個變量在函數(shù)定義的括號內(nèi),所以它們是局部變量,因此對于全局的location和game-map沒有關系.
注意到了嗎?Lisp中的函數(shù)與其它語言中的函數(shù)定義相比,更像是數(shù)學中的函數(shù):它不打印信息或者彈出消息框:它所作的就是返回結果.
我們假設現(xiàn)在我們在起居室里!
為了能找到起居室的描述,describe-locatin函數(shù)首先需要從地圖中找到起居室.(location game-map)就是進行從game-map中查找內(nèi)容的,并返回起居室的描述.然后first命令來處理返回值,取得返回的list的第一個元素,這個就是起居室的描述了. 現(xiàn)在我們來測試一下
(describe-location 'living-room game-map)
user=> (describe-location 'living-room game-map)
(you are in the living-room of a wizard's house -
there is a wizard snoring loudly on the couch -)
很完美!這就是我們要的結果!請注意我們在living-room前添加了一個單引號,因為這個符號是地點map的一個名稱!但是,為什么我們沒有在game-map前面添加單引號呢?這是因為我們需要編譯器去查詢這個符號所指向的數(shù)據(jù)(就是那個map)
函數(shù)式編碼風格
你可能已經(jīng)發(fā)現(xiàn)了describe-location函數(shù)有幾個讓人不太舒服的地方.
第一,為什么要傳遞位置和map參數(shù),而不是直接使用已經(jīng)定義的全局變量?原因是Lisp程
序員喜歡寫函數(shù)式風格的代碼.函數(shù)式風格的代碼,主要遵循下面三條規(guī)則:
- 只讀取函數(shù)傳遞的參數(shù)或在函數(shù)內(nèi)創(chuàng)建的變量
- 不改變已經(jīng)被設值的變量的值
- 除了返回值,不去影響函數(shù)外的任何內(nèi)容
你也許會懷疑在這種限制下你還能寫代碼嗎?答案是:可以!為什么很多人對這些規(guī)則感到疑惑呢?一個很重要的原因是:遵循此種風格的代碼更加的引用透明(referential transparency):這意味著,對于給定的代碼,你傳入相同的參數(shù),永遠返回相同的結果---這能減少程序的錯誤,也能提高程序的生產(chǎn)力!
當然了,你也會有一些非函數(shù)式風格的代碼,因為不這么做,你無法和其它用戶或外部內(nèi)容進行交互.教程后面會有這些函數(shù),他們不遵循上面的規(guī)則.
describe-location函數(shù)的另一個問題是,它沒告訴我們怎么進入一個位置或者怎么從某個位置出來.讓我們來編寫這樣的函數(shù):
(defn describe-path [path]
`(there is a ~(second path) going ~(first path) from here -))
這個函數(shù)看起來很明了:它看起來更像是數(shù)據(jù)而不是函數(shù).我們先來嘗試調(diào)用它,看它做了些什么:
(describe-path '(west door garden))
user=> (describe-path '(west door garden))
(user/there user/is user/a door user/going west user/from user/here clojure.core/-)
這是什么?!結果看起來很亂,包含了很多的/和一些其它的文字!這是因為Clojure會將命名空間的名字添加到表達式的前面.我們這里不深究細節(jié),只給你提供消除這些內(nèi)容的函數(shù):
(defn spel-print [list] (map (fn [x] (symbol (name x))) list))
修改調(diào)用方式
(spel-print (describe-path '(west door garden)))
user=> (spel-print (describe-path '(west door garden)))
(there is a door going west from here -)
現(xiàn)在結果很清晰了:這個函數(shù)接收一個描述路徑的list然后將其解析到一個句子里面.我們回過頭來看這個函數(shù),這個函數(shù)和它產(chǎn)生的數(shù)據(jù)非常的像:它就是拼接第一個和第二個list的元素到語句中!它是怎么做到的?使用語法quote!
還記得我們使用quote來從代碼模式切換到數(shù)據(jù)模式嗎?語法quote的功能類似,但是還不只這樣.在語法quote里,我們還能使用'~'再次從數(shù)據(jù)模式切換回代碼模式.
語法quote是List的一個很強大的功能!它能使我們的代碼看起來像它創(chuàng)建的數(shù)據(jù).這在函數(shù)式編碼中很常見:創(chuàng)建這種樣子的函數(shù),使得我們的代碼更易讀也更穩(wěn)健:
只要數(shù)據(jù)不變,函數(shù)就不需要修改.想象一下,你能否在VB或C中編寫類似的代碼?你可能需要將文字切成小塊,然后在一點點的組裝-這和數(shù)據(jù)本身看起來差距很大,更別說代碼的穩(wěn)健性了!
現(xiàn)在我們能描述一個路徑,但是一個地點可能會有多個路徑,所以讓我們來創(chuàng)建一個函數(shù)叫做describe-paths:
(defn describe-paths [location game-map]
(apply concat (map describe-path (rest (get game-map location)))))
這個函數(shù)使用了另一個在函數(shù)式編程中很常用的技術:高階函數(shù).apply和map這兩個函數(shù)能將其它的函數(shù)作為參數(shù).map函數(shù)將另一個函數(shù)分別作用到list中的每個對象上,這里是調(diào)用describe-path函數(shù).apply concat是為了減少多余的括號,沒有多少功能性操作!我們來試試新函數(shù)
(spel-print (describe-paths 'living-room game-map))
user=> (spel-print (describe-paths 'living-room game-map))
(there is a door going west from here -
there is a stairway going upstairs from here -)
漂亮!
最后,我們還剩下一件事要做:描述某個地點的某個對象!我們先寫個幫助函數(shù)來告訴我們在某個地方是否有某個對象!
(defn is-at? [obj loc obj-loc] (= (obj obj-loc) loc))
=也是個函數(shù),它判斷對象的地點是否和當前地點相同!
我們來嘗試一下:
(is-at? 'whiskey-bottle 'living-room object-locations)
user=> (is-at? 'whiskey-bottle 'living-room object-locations)
true
返回結果是true,意味著whiskey-bottle在起居室.
現(xiàn)在讓我們來使用這個函數(shù)描述地板:
(defn describe-floor [loc objs obj-loc]
(apply concat (map (fn [x]
`(you see a ~x on the floor -))
(filter (fn [x]
(is-at? x loc obj-loc)) objs))))
這個函數(shù)包含了很多新事物:首先,它有匿名函數(shù)(fn定義的函數(shù)).第一個fn干的事,和下面的函數(shù)做的事情是一樣的:
(defn blabla [x] `(you see a ~x on the floor.))
然后將這個blabla函數(shù)傳遞給map函數(shù).filter函數(shù)是過濾掉那些在當前位置沒有出現(xiàn)的物體.我們來試一下新函數(shù):
(spel-print (describe-floor 'living-room objects object-locations))
user=> (spel-print (describe-floor 'living-room objects object-locations))
(you see a whiskey-bottle on the floor - you see a bucket on the floor -)
現(xiàn)在,讓我們來將這些函數(shù)串聯(lián)起來,定義一個叫l(wèi)ook的函數(shù),使用全局變量(這個函數(shù)就不是函數(shù)式的了!)來描述所有的內(nèi)容:
(defn look []
(spel-print (concat (describe-location location game-map)
(describe-paths location game-map)
(describe-floor location objects object-locations))))
我們來試一下:
user=> (look)
(you are in the living room of a wizards house -
there is a wizard snoring loudly on the couch -
there is a door going west from here -
there is a stairway going upstairs from here -
you see a whiskey-bottle on the floor -
you see a bucket on the floor -)
很酷吧!
環(huán)游我們的游戲世界
好了,現(xiàn)在我們能看我們的世界了,讓我們來寫一些代碼來環(huán)游我們的世界.walk-direction包含了一些方向可以使我們走到那里:
(defn walk-direction [direction]
(let [next (first (filter (fn [x] (= direction (first x)))
(rest (location game-map))))]
(cond next (do (def location (nth next 2)) (look))
:else '(you cannot go that way -))))
這里的let用來創(chuàng)建局部變量next,用來描述玩家的方向.rest返回一個list,包含原list中除了第一個元素外的全部元素.如果用戶輸入了錯誤的方向,next會返回
().
cond類似于if-then條件:每個cond都包含一個值,lisp檢查該值是否為真,如果為真則執(zhí)行其后的動作.在這里,如果下一個位置不是nil,則會定義玩家的location到新位置,然后告訴玩家該位置的描述!如果next是nil,則告訴玩家,無法到達,請重試:
(walk-direction 'west)
user=> (walk-direction 'west)
(you are in a beautiful garden -
there is a well in front of you -
there is a door going east from here -
you see a frog on the floor -
you see a chain on the floor -)
現(xiàn)在,我們通過創(chuàng)建look函數(shù)來簡化描述.walk-direction也是類似的功能.但是它需要輸入方向,而且還有個quote.我們能否告訴編譯器west僅僅是個數(shù)據(jù),而不是代碼呢?
構建SPELs
現(xiàn)在我們開始學習Lisp中一個很強大的功能:創(chuàng)建SPELs!SPEL是"語義增強邏輯"的簡稱,它能夠從語言級別,按照我們的需求定制,對我們的代碼添加新的行為-這是Lisp最為強大的一部分.為了開啟SPELs,我們需要先激活Lisp編譯器的SPEL
(defmacro defspel [& rest] `(defmacro ~@rest))
現(xiàn)在,我們來編寫我們的SPEL,叫做walk:
(defspel walk [direction] `(walk-direction '~direction))
這段代碼干了什么?它告訴編譯器walk不是實際的名稱,實際的名字叫walk-direction,并且direction前面有個quote.SPEL的主要功能就是能在我們的代碼被編譯器編譯之前插入一些內(nèi)容!
注意到了嗎?這段代碼和我們之前寫的describe-path很類似:在Lisp中,不只是代碼和數(shù)據(jù)看起來很像,代碼和特殊形式對于編譯器來說也是一樣的-高度的統(tǒng)一帶來簡明的設計!我們來試試新代碼:
(walk east)
user=> (walk east)
(you are in the living room of a wizards house -
there is a wizard snoring loudly on the couch -
there is a door going west from here -
there is a stairway going upstairs from here -
you see a whiskey-bottle on the floor -
you see a bucket on the floor -)
感覺好多了! 現(xiàn)在我們來創(chuàng)建一個命令來收集游戲里的物品
(defn pickup-object [object]
(cond (is-at? object location object-locations)
(do
(def object-locations (assoc object-locations object 'body))
`(you are now carrying the ~object))
:else '(you cannot get that.)))
這個函數(shù)檢查物品是否在當前地點的地上-如果在,則將它放到list里面,并返回成功提示!否則提示失敗! 現(xiàn)在我們來創(chuàng)建另一個SPEL來簡化這條命令:
(defspel pickup [object] `(spel-print (pickup-object '~object)))
調(diào)用
(pickup whiskey-bottle)
user=> (pickup whiskey-bottle)
(you are now carrying the whiskey-bottle)
現(xiàn)在我們來添加更多有用的命令-首先,一個能讓我們查看我們撿到的物品的函 數(shù):
(defn inventory []
(filter (fn [x] (is-at? x 'body object-locations)) objects))
以及一個檢查我們是否有某個物品的函數(shù):
(defn have? [object]
(some #{object} (inventory)))
創(chuàng)建特殊操作
現(xiàn)在我們只剩下一件事情需要做了:添加一些特殊動作,使得玩家能夠贏得游戲.第一條命令是讓玩家在閣樓里給水桶焊接鏈條.
(def chain-welded false)
(defn weld [subject object]
(cond (and (= location 'attic)
(= subject 'chain)
(= object 'bucket)
(have? 'chain)
(have? 'bucket)
(not chain-welded))
(do (def chain-welded true)
'(the chain is now securely welded to the bucket -))
:else '(you cannot weld like that -)))
首先我們創(chuàng)建了一個新的全局變量來進行判斷,我們是否進行了此操作.然后我們創(chuàng)建了一個weld函數(shù),來確認此操作的條件是否完成,如果已完成則進行此操作.
來試一下:
(weld 'chain 'bucket)
user=> (weld 'chain 'bucket)
(you cannot weld like that -)
Oops...我們沒有水桶,也沒有鏈條,是吧?周圍也沒有焊接的機器!
現(xiàn)在,讓我們創(chuàng)建一條命令來將鏈條和水桶放到井里:
(def bucket-filled false)
(defn dunk [subject object]
(cond (and (= location 'garden)
(= subject 'bucket)
(= object 'well)
(have? 'bucket)
chain-welded)
(do (def bucket-filled true)
'(the bucket is now full of water))
:else '(you cannot dunk like that -)))
注意到了嗎?這個命令和weld命令看起來好像!兩條命令都需要檢查位置,物體和對象!但是它們還是有不同,以至于我們不能將它們抽到一個函數(shù)里.太可惜了!
但是...這可是Lisp.我們不止能寫函數(shù),還能寫SPEL!我們來創(chuàng)建了SPEL來處理:
(defspel game-action [command subj obj place & args]
`(defspel ~command [subject# object#]
`(spel-print (cond (and (= location '~'~place)
(= '~subject# '~'~subj)
(= '~object# '~'~obj)
(have? '~'~subj))
~@'~args
:else '(i cannot ~'~command like that -)))))
非常復雜的SPEL!它有很多怪異的quote,語法quote,逗號以及很多怪異的符號!更重要的是他是一個構建SPEL的SPEL!!即使是很有經(jīng)驗的Lisp程序員,也需要費下腦細胞才能寫出這么個玩樣!!(這里我們不管)
這個SPEL的只是向你展示,你是否夠聰明來理解這么復雜的SPEL.而且,即使這段代碼很丑陋,如果它只需要寫一次,并且能生成幾百個命令,那么也是可以接受的!
讓我們使用這個新的SPEL來替換我們的weld命令:
(game-action weld chain bucket attic
(cond (and (have? 'bucket) (def chain-welded true))
'(the chain is now securely welded to the bucket -)
:else '(you do not have a bucket -)))
現(xiàn)在我們來看看這條命令變得多容易理解:game-action這個SPEL使得我們能編寫我們想要的核心代碼,而不需要額外的信息.這就像我們創(chuàng)建了我們自己的專門創(chuàng)建游戲命令的編程語言.使用SPEL創(chuàng)建偽語言稱為領域特定語言編程(DSL),它使得你的編碼更加的快捷優(yōu)美!
(weld chain bucket)
user=> (weld chain bucket)
(you do not have a chain -)
...我們還沒有做好焊接前的準備工作,但是這條命令生效了!
下面我們重寫dunk命令:
(game-action dunk bucket well garden
(cond chain-welded
(do (def bucket-filled true)
'(the bucket is now full of water))
:else '(the water level is too low to reach -)))
注意weld命令需要檢驗我們是否有物體,但是dunk不需要.我們的game-action這個SPEL使得這段代碼易寫易讀.
最后,就是將水潑到巫師身上:
(game-action splash bucket wizard living-room
(cond (not bucket-filled) '(the bucket has nothing in it -)
(have? 'frog) '(the wizard awakens and sees that you stole
his frog -
he is so upset he banishes you to the
netherworlds - you lose! the end -)
:else '(the wizard awakens from his slumber and greets you
warmly -
he hands you the magic low-carb donut - you win!
the end -)))
現(xiàn)在你已經(jīng)編寫完成了一個文字冒險游戲了!
點擊這里是完整的游戲.
點擊這里是代碼.
為了使教程盡可能的簡單,很多Lisp的執(zhí)行細節(jié)被忽略了,所以最后,讓我們來看看這些細節(jié)!
附錄
現(xiàn)在,我們來聊一聊被忽略的細節(jié)!
首先,Clojure有一套很成熟的定義變量以及改變變量值的系統(tǒng).在此教程中,我們只使用了def來設置和改變?nèi)肿兞康闹?而在真正的Clojure代碼里,你不會這么做.取而代之,你會使用Refs,Atoms和Agents,它們提供了更清晰,以及線程安全的方式來管理數(shù)據(jù).
另一個問題就是我們在代碼中大量使用了符號(symbol)
'(this is not how Lispers usually write text)
"Lispers write text using double quotes"
符號在Clojure有特殊含義,主要是用來持有函數(shù),變量或其它內(nèi)容的.所以,在Lisp中將符號作為文本信息描述是很奇怪的事情!使用字符串來顯示文本信息可以避免這樣的尷尬!不過,使用字符串的話,在教程里就沒法講很多關于符號的內(nèi)容了!
還有就是SPEL在Lisp里面更普遍的叫法是"宏",使用defmacro來定義,但是這個名字不易于教學,所以沒有提及.你可以閱讀此文,這是我為什么沒有使用"宏"這個名字的原因.
最后,在編寫類似game-action這樣的SPEL的時候,很可能會發(fā)生命名重復的問題.當你編寫了足夠多的lisp的時候,你會越來越能體會到這個問題了.
Q. 后面我該閱讀哪些內(nèi)容來擴充我的Lisp知識? A.
在cliki網(wǎng)站有很多Lisp書籍可以下載.
如果你對于理論上的內(nèi)容很感興趣,那么我推薦Paul Graham的 On Lisp電子書,它是免費的.他網(wǎng)站上的一些短文也很精彩.
如果你對實際應用比較感興趣,那么大多數(shù)Lisp程序員對Perter Seibel編寫的"Practical Common Lisp"這本書推崇有加,你可以從這里獲得
為什么沒有使用"宏"這個詞
編寫這個教程的一個意圖是使用宏來解決真實的難題.而經(jīng)常的,當我向沒有Lisp經(jīng)驗的人解釋宏這個概念的時候,我得到的答復往往是,"哦!C++里也有宏".當發(fā)生這種事情的時候,我就很難去解釋宏的概念了.的確,Lisp中的宏和C++中的宏的確有幾分相似,它們都是為了能通過編譯器來改進代碼的編寫...
...所以,假設一下,如果John McCarthy使用了"add"而不是"cons"這個詞來將元素添加到list中:我就真的很難解釋cons是如何工作的了!
所以,我決定在此文中使用一個新的詞匯:SPEL,語義增強邏輯的簡稱,它更易理解
一些:
- 它解釋了Lisp宏的核心功能,能改變Lisp運行環(huán)境的行為
- SPEL這個術語可以被用來很高雅的解釋很多語言上觀念.
- 這個術語不會導致Lisp中的宏與其它的宏被混為一談
- SPEL這個詞重復的可能性非常低.Google搜索"macro 或者 macros 程序 -lisp -scheme"返回大概1150000條結果.而搜索"spel 或者 spels 程序 -lisp -scheme"值返回28400條結果.
所以,我希望,作為一個Lisp程序員,你能接受這個術語.當然了,像這樣的新詞匯會被接受的可能性非常低.
如果你有一個庫或者是一個Lisp實現(xiàn)者,請先放下你手頭上的工作,先在你的庫里,添加下面這行代碼:
(defmacro defspel [& rest] `(defmacro ~@rest))
譯者感想
- 本人對Lisp的宏還是有些了解的,所以個人無法接受SPEL這個新詞匯
- 且SPEL使得代碼不易閱讀,就game-action這個SPEL來說,使用了兩層,而使用宏只需要一層
- 附錄中是我使用Clojure的慣用法重新改寫的代碼,且文字翻譯成了中文.以及使用了宏而不是SPEL.各位可比較,自行選擇