使用Clojure編寫文字冒險游戲

本文翻譯自: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.各位可比較,自行選擇

源代碼

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市浓镜,隨后出現(xiàn)的幾起案子遍略,更是在濱河造成了極大的恐慌贵扰,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件撒汉,死亡現(xiàn)場離奇詭異展父,居然都是意外死亡,警方通過查閱死者的電腦和手機唉窃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來荷鼠,“玉大人,你說我怎么就攤上這事榔幸≡世郑” “怎么了?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵削咆,是天一觀的道長牍疏。 經(jīng)常有香客問我,道長拨齐,這世上最難降的妖魔是什么鳞陨? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上厦滤,老公的妹妹穿的比我還像新娘援岩。我一直安慰自己,他們只是感情好掏导,可當我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布享怀。 她就那樣靜靜地躺著,像睡著了一般趟咆。 火紅的嫁衣襯著肌膚如雪添瓷。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天值纱,我揣著相機與錄音鳞贷,去河邊找鬼。 笑死虐唠,一個胖子當著我的面吹牛搀愧,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播凿滤,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼妈橄,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了翁脆?” 一聲冷哼從身側響起眷蚓,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎反番,沒想到半個月后沙热,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡罢缸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年篙贸,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片枫疆。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡爵川,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出息楔,到底是詐尸還是另有隱情寝贡,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布值依,位于F島的核電站圃泡,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏愿险。R本人自食惡果不足惜颇蜡,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧风秤,春花似錦鳖目、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽纵苛。三九已至挺狰,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留薪贫,地道東北人。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓刻恭,卻偏偏與公主長得像瞧省,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子鳍贾,可洞房花燭夜當晚...
    茶點故事閱讀 44,724評論 2 354

推薦閱讀更多精彩內(nèi)容