Clojure Crash Tutorial
- Clojure 和 LISP 的關係
- Literal Representation Data
- 註解
- LISP FORM
- List
- Collection
- Function
- Special Form
- What Macro ?
- Flow Control
- Side Effect
- 重新回顧 Clojure Verbs:LISP Form、Special Form、Macro
- 結語
- Code Block 的小袐密
Clojure 和 LISP 的關係
在語法上,目前台面上看到的大部份主流語言都深受 C 影響,如 Java、JavaScript、C#、PHP 等, 而 C 的語法深受另一個更早期的語言影響 – ALGOL。 另外和 ALOGL 同時期,也有個語言也影響現代語言,目前語言常見的功能,像是 High Order Function、Garbage Collection、REPL、Recursion 等, 最早是在 LISP 中出現的。
LISP 是 LISt Processor 的簡寫,最大的特點就是它的語法:
(car (cdr '(1 2 3 4 5)))
這種語法而上面這段程式所做的行為是:
- 把
'(1 2 3 4 5)
這個資料(這是一個 List)做為cdr
這個 Function 輸入 - 把步驟 1 的輸出再當成參數輸入到
car
這個 Function 中
Clojure 是一個 LISP 的方言,所以 Clojure 的語法就前一個例子那樣,只不過和 LISP 不同的是:
- 用字更現代,不會有義意不明的名稱如:car、cdr 等
- 吸收了一些 Functional Programming 後來所發展的功能,像是 Immutable Data Structure
- 運作在 JVM 上,所以還有和 Java 互動的功能
- Clojure 沒有 cons cell
- ...
由於本文主要針對連 LISP 都沒聽過的受眾,如果是 LISPer 想了解更多可以直接參考 Clojure Official 的 Differences with other Lisps。
p.s. car 和 cdr 其實是 contents of the address part of register number
和 contents of the decrement part of register number
,這其實是在說早期的電腦硬體架構
Literal Representation Data
前述給了一個 LISP 的程式例子:
(car (cdr '(1 2 3 4 5)))
應該很容易注意到 LISP 中是直接用空白
分隔東西的,在 LISP 中這些東西有一個專有名稱叫atom
,是指說無法再被分割的資料, 反之 list 就是可以被分割的資料:
car ; <- 這是 atom
1 ; <- 這還是 atom
(1 2) ; <- 這是 list
() ; <- 空的 list,因為他不能被分割了,所以是 atom
但在 Clojure 中,atom 是另一個和 Concurrency 有關的名稱,所以 Clojure 不會稱那些東西
為 atom
,那我們要如果稱呼這些東西? 以官方的文件續述來看,會叫這些東西為 Element
,中譯元素
(參考 Microsoft 語言入口網站),這個名稱也符合較為新的語言如 JavaScript, Python 等對其資料結構內容物的稱呼, Clojure 在很多名稱上去除了古老 LISP 的用詞。
再來讓我們看看這些元素
,元素有型態和值,比方說 1
表示他是值為 1
的 Integer
,Clojure 在程式語言的分類中,是屬於 強型別
的語言, 也就是在運算中不會任意幫使用者更動原本元素該有的型態,一定要轉型才可以,如以下例子就會出錯:
(+ "number = " 1)
而以下例子則正確:
(str "number = " 1)
原因是 str
Function 有把進來的參數轉成字串。
因為 Clojure 寄身在 Java 上,在 Clojure 其實也可以用 Java Object 的方式定義資料,但目前我們只先關心 Literal Data Type, 所謂 Literal,是用特定符號表達程式中資料的寫法, 如前面的例子中 1
表示 Integer,"nuber = "
表示 String。
Numeric
在 Clojure 中,Literal 的寫法可以表示的數字型態有:
- Integer
- Float Point
- Big Number
- Ratio
Character, String And Regular Expression
Symbol & Keyword
註解
註解不是程式碼,是為了在解式碼間記錄一些資訊,可能是程式的說明。 Clojure 的註解用 ;
Semicolon 做為開頭,在 ;
後面的字串都不會被當做程式碼。
一般在 Clojure 的程式碼中,常常會看到 ;
、;;
、;;;
這三種樣子的註解開頭,在慣例上只是為了表示三種不同階級的註解,功能上並沒有差別。
;
表示單行註解;;
表示某個程式區塊的註解;;;
表示檔案的註解
LISP FORM
在 Clojure 中,一段程式的寫法為:
(str "Hello" " " "World" "!")
而一組 List Collection 的資料結構為:
'(str "Hello" " " "World" "!")
雖然都是用小括號的寫法(
)
,但差異在左括號前有沒有 Quote
('
),若左括號前有 '
時, 該 List 不會被 Eval
,也就是不會被當成程式碼執行,'( <任意元素> )
的這個寫法也可以用當成程式碼寫的方法:
'(1 2 3 4 5)
用 Quote Special Form
也會得到和使用 '
相同結果:
(quote (1 2 3 4 5))
'( )
是 (quote ( ))
的簡易寫法,正式名稱為 Quote Special Form
,是 Special Form
的其中一種, 而相對於 Special Form
的就是 Lisp Form
了。
左括號右邊的第一個東西會被當成 Function,第二個之後的東西會被當成參數的輸入進到 Function 中, 這是 LISP 的基本規則,也就是 LISP Form:
(+ 1 2 3 4 5)
這樣寫會把 1 當成 Function,2 3 4 5 當成參數,想當然會出錯,因為 1 不是 Function
(1 2 3 4 5)
LISP Form 再更精確說明,應該是 對一個 List 求值(Eval)
, 假設有一個 List 如下:
(* 2 3)
括號中,第一個元素為*
,第二個和第三個元素為 2
和 3
,當 Clojure 對這個 List 求值時會有以下步驟:
- 確認第一個元素是否為 Function,在本例是中
*
是一個 function,因此進行 LISP Form 的規則。 - 對第二個元素求值,得到
2
- 對第三個元素求值,得到
3
- 第二和第三個元素求值的結果作為輸入參數到第一個元素所表示的 Function
- 整個 List 求值的結果為
6
也許你會有點疑惑:“為什麼一個數字 2 還需要被求值,他不是看起來就是 2 嗎?”,會有這個問題也是正常的,在其它語言中沒有這種“求值”的動作, 在 Clojure (或者說 LISP 系的語言)中會有一個求值動作的原因是,在 Compile 前還會有一個 Reader 解析 List, Reader 看到有加 '
的 Symbol 或 List 才會知道不需要對他們求值。
另外,像是 2
這樣的基本元素 ( 2
是 Integer),對它們求值還是會得到它們本身:
(eval 2) ; => 2
p.s. eval
除了表達 Evaluate 這個外,也是一個可以用的 Function。
再來一個更複雜的例子:
(+ 1 (* 2 3) 4 5)
求值步驟如下:
- 確認第一個元素是否為 Function,在本例是中
+
是一個 Function,因此進行 LISP Form 的規則。 - 對第二個元素求值得到
1
- 對第三個元素求值...這時候問題來了,第三個元素是一個 List:
這時 Clojure 該怎麼做? Clojure 會優先對這個 List 求值,完成後才會對下一個元素求值,求值的結果為(* 2 3)
6
。 - 對第四個元素求值得到
4
- 對第五個元素求值得到
5
到步驟 5. 時,原 List 變成:(+ 1 6 4 5)
- 元素二到五當為 Function
+
的參數求值結果為16
在這個例子不難看出,LISP Form 的 Eval 順序是 Depth-First:

LISP Form 是 Clojure 的基本規則,LISP 的其中一個 動作
(Verb), 除了 LISP Form 外,在接下來還會介紹到 Special Form
和 Macro
。
List
前面已經介紹過 LISP Form
和 Quote Special Form
,到此我們已經理解在 Clojure 中如果不想對資料求值, 只想表示該資料為一個 List 時,只要加上 Quote
即可,也就是說的作用就是不求值(Delaying evaluation), 從這點不難理解為什麼 LISP 世界的人總是在說 Code is data, data is code.
, Data (list) 就加 Quote
,Code (eval) 不加 Quote
,也因為這個特性,了解怎麼操作 List 就變得相當重要。
一個操作 List 的例子:
(eval (cons '* (rest '(+ 1 2 3 4 5))))

'(+ 1 2 3 4 5)
因為加了 '
(Quote),因此這是一組資料 (List), rest
是我們要介紹的第一個 list 操作,功能是取出 list 中,除了第一個元素之外的其它元素,得到新的 list 回傳:
(rest '(+ 1 2 3 4 5))
cons
也是 list 操作,功能是幫 list 加入新元素,要注意的是第一個參數是新元素,而第二個參數才是 list:
(cons '* '(1 2 3 4 5))
另外要注意的是因為 list 特性是一個串一個(Linked List),新元素放到第一個成本最小,所以前例的結果才會是 (* 1 2 3 4 5)
。
最後到了 eval
,被 Eval 的 list 為:
(eval '(* 1 2 3 4 5))
原本的 +
經過了 rest
和 cons
後,被轉換成 *
,對 Clojure 來說, 程式碼可以很輕易的操作成另一個樣子,而這還不是 Clojure 最強大的特性,但到這邊我們先就此打住,優先關心 list 的操作。
Functions of List
以下是常用的幾個 Function:
- first
- rest
- last
- butlast
- cons
- conj
- count
- reverse
- map
- filter
- reduce
first
、rest
、last
、butlast
這四個 Function 可以視為同一組:
cons
和 conj
都是可以加入新元素的 Function,cons
請把它想成是 first 和 rest 的反向操作,假設原 list 為 (1 2 3 4 5)
, first 的結果為 1
,rest 的結果為 (2 3 4 5)
,cons 就是將 1
和 (2 3 4 5)
合併回去的方法:
(cons 1 '(2 3 4 5))
而 conj
可以把它想成是多次的 cons
,cons 的參數只有兩個,first 的元素和 rest 的 list,在 conj 中,第一個參數就是 list 了,而接下來的參數則是元素:
(conj '(2 3 4 5) 1 0 -1 -2)
如果 conj 只給一個元素,那結果會和 cons 一樣:
(conj '(2 3 4 5) 1)
p.s. cons
是 Clojure 中少數和以前 LISP 名稱和功能也一樣的 symbol,它是 construct
的縮寫。
count
回傳 list 中有多少個元素:
(count '("Apple" "Orange" "Banana" "Churry"))
reverse
將 list 的內容順序反轉:
(reverse '(1 2 3 4 5))
map
、filter
、reduce
這三個 Function 應該是最近 Functional Programming 正夯的清況下, 己經廣為人知的 Function 吧(這都要感謝 JavaScript),在 Twitter 有一則很有趣的貼文用了 Emoji 來比喻這三個 Function 的行為(雖然是 JavaScript 的例子):
Map/filter/reduce in a tweet:
— Steven Luscher (@steveluscher) June 10, 2016
map([🌽, 🐮, 🐔], cook)
=> [🍿, 🍔, 🍳]
filter([🍿, 🍔, 🍳], isVegetarian)
=> [🍿, 🍳]
reduce([🍿, 🍳], eat)
=> 💩
JavaScript 中用 []
的資料結構叫 Array
,在這邊我們用 Collection 表示泛指非 Key-Value 類型的資料結構, JavaScript 的 Array
和 Clojure 的 List
都用 Collection 來代稱。
map
的例子中,(🌽 🐮 🐔)
經過 cook
這個 Function 之後(先不管 cook 的實作是什麼), 最後得到一個新的 collection (🍿 🍔 🍳)
,map
的行為就是把每個元素都放到 cook 中並把結果收集起來:
- 🌽 -> cook(🌽) -> 🍿
- 🐮 -> cook(🐮) -> 🍔
- 🐔 -> cook(🐔) -> 🍳
Clojure 的 map
和 Twitter 這個 JavaScript 的例子有點不一樣的地方在 map 的參數位置,在 JavaScript 中, Collection 是放在第一個參數,Function 放第二個,而 Clojure Function 放在第一個參數,Collection 才是放第二個參數:
(map inc '(1 2 3 4 5))
inc
Function 的功能是將數字加 1,(1 2 3 4 5)
中每一個數字經過 inc
後, 得到新的 list (2 3 4 5 6)
。
filter
的 Function 和 map 不太一樣,map 並不限制 Function 回傳的類型,而 filter 的 Function 參數是要求回傳 Boolean
, 而只有回傳值為 true
的元素才會被收集到新的 Collection,就拿 Twitter 的那個例子來說,Function isVegetarian
是判斷是否為素食(這個例子是蛋奶素), 而只有爆米花和蛋符合,所以新的 Collection 只有 (🍿 🍳)
:
- 🍿 -> isVegetarian(🍿) -> true
- 🍔 -> isVegetarian(🍔) -> false
- 🍳 -> isVegetarian(🍳) -> true
reduce
可能是這三個 Function 中對初學者最難理解的一個了,而就 Twitter 上那個 JavaScript 的範例來說,它其實不是一個好例子, 原因是 reduce 的 Function 參數有兩個參數,第一個位置的參數通常叫 accumulator
,第二位置叫 element
或 current value
, 當然 reduce function 的參數可能因為不同語言和不同 Library 有所差異,例如 JavaScript 的 Array reduce 中的 reduce function 就多了第三個位置的參數叫 current index , 但不影響 redure 最重要的目的:把 Collection 內的元素傳入 reduce function 中,取得新的值再將該值和下一個元素傳入 reduce function,重複這個動作直到沒有下個元素, 就 Twitter 的例子來看:
- eat(null, 🍿) -> 💩
- eat(💩, 🍳) -> 💩
- 💩
步驟 2. 回傳和原本的 accumulator
相同,還是一個 💩,因為 Emoji 中沒有更大的 💩,這個例子有趣但有點失真,雖然有這一點點小缺點,以解理 reduce 來說算是一個不差的例子了。
Clojure 的 reduce
也和 map
、filter
的差不多,functino 在前,list 在後,過 reduce 有一個情況是 accumulator 的起始值,因此 reduce 有兩種情況:
- (reduce f coll)
- (reduce f val coll)
f
表示 function,coll
表示 Collection,在這邊就是 list ,而 val
為要設定給 accumulator 的起始值,以下例子:
(reduce + '(1 2 3 4 5))
設定起始值的例子:
(reduce + 10 '(1 2 3 4 5))
沒有設定起始的情況下,完全依賴 reduce function 怎麼做,以 +
這個 function 來看,沒有起始值,所以 accumulator 為 nil,element 為 1, 在 +
實作細結中如果第一個參數是 nil,也只是把它當成 0 而己:
(+ nil 1)
Collection
Sequencetial Collection
Hashed Collection
Function
Special Form
在 LISP FORM 的部份有提到 Quote Special Form
,而 Quote Special Form 的功能是跳過求值, 在 Clojure 中 Special Form 是語言內建的定義,我們不能定義新的 Special Form,可以把他們當 Clojure 基本的語法來看待, 其本的 Special Form 有:
- def
- if
- do
- let
- quote
- fn
- loop & recur
另外還有幾個暫不說明的:var、try、throw、monitor-enter、monitor-exit。
既然叫 "Special Form",那當然不會是 LISP Form 那樣 Depth-First 的 Eval 規則了, 有像我們己經看過的 Quote Special Form
,反而是不 Eval list,也有像 if
的程式分枝, 還有 def
和 let
用來定義 symbol,Special Form 是 Clojure 中的基本功能,只要有這幾個基本的 Special Form, 整個 Clojure 的生態就能用 Macro 和 Function 建構出來。
1. def
def 是 define 的意思,Clojure 文件中寫了:(def symbol doc-string? init?)
該開始看 Clojure 的文件可能不太了解這是什麼意思,在 Clojure 的文件常常會有這種“黑話”,在這邊先解釋一下這個黑話的意思:
def
就是我們想了解的這個 Special Form 名稱,不需要再特別說明。symbol
表示這個元素要放的是一個合法的 symbol,忘了 symbol 是什麼可以回到 Symbol & Keyword。doc-string?
表示這個元素會被當成是 Document,而它是一個 string,加了?
表示它可有可無。init?
表示這是一個初始值,沒有特定型態,因為最後也是一個?
,表示它可有可無。
Clojure 官方的文件風格一開始可能不是很能理解,常常還是會遇到不清楚該放什麼東西當參數, 所以 Clojure 的使用者還有另一個地方可以看說明:ClojureDocs/clojure.core/def, ClojureDocs 是一個社群建立的文件網站,最主要的功能是還有提供 Clojure 社群使用者們分享的範例。
回到 def
,以下是 def 不同參數情況的範例: 如果只有一個參數 symbol 時,是在 Clojure runtime 中宣告該 symbol 有被定義,但值會是 nil:
(def gdp)
當只給兩個參數時,第二個參數會被當成是該 symbol 的初始值:
(def gdp 0)
當給三個參數時,第二個參數為說明文件而不是初始值,第三個參數才是:
(def gdp
"在描述地區性生產時稱本地生產總值或地區生產總值,是一定時期內(一個季度或一年),
一個區域內的經濟活動中所生產出之全部最終成果(產品和勞務)的市場價值(market value)。"
0)
doc-string 的功用:
(doc gdp)
doc 是用來看各 symbol 寫好的 doc-string,在開發中, 常常會在 REPL 介面上用這個 Function 看 Clojure 的文件(不論是 Clojure 官方的還是第三方的,只要有寫 doc-string 都可以看到說明文字)。
如果給三個參數,但第二個參數不是字串時,Clojure 會出錯:
(def gdp
123
0)
2. if
if
是用來產生程式分枝的 Special Form,文件格式的說明為:(if test then else?)
, if Special Form 會先對 test
求值,看結果是否為真
,如果為真,會對 then
求值, 不為真則對 else?
求值,當然在前面提過,else? 有 ?
表示它可有可無,所以如果 test 不為真, 也有可能不再做任何求值。
那為什麼 test
不寫成 perd
(predict function) 就好?原因是在 if
的 test 是否為真是一個比較特別的判斷方式, 並不只是 Boolean 值而已,以下的值都為真
:
不為真
的有:
3. do
do
在文件中格式說明為:(do expr*)
,其中 expr
的意思為 s-expression,結尾加 *
表示有 0~n 個, 這種表示法很像 Regular Expression 的寫法,do 的效果是對這些 s-exp 求值,最終回傳值為最後一條 s-exp 的回傳。
以下例子:
(do
"I can show you the world,"
(range 100)
(def locale (nth ["嘉義" "雲林" "台南" "高雄"] (rand-int 4)))
(def gdp (rand-int 100000))
(if (and (= locale "高雄")
(> gdp 64000))
"發大財"
"北漂"))
4. let
let
在文件中的格式說明為:(let [bindings*] exprs*)
,let 的結構比較特別一點, [bindings*]
的意思是一組 vector,內容是 symbol 的 binding,例如:
[a 0
b "hello"]
binding 後,symbol a
和 b
就可以在 let 中的 s-exp 中存取到 a
和 b
,完整的例子:
(let [var1 "value1"
var2 "value2"]
(str var1 " " var2))
但要注意的是,在 let 外,var1 和 var2 無法被求值:
var1
var2
5. quote
quote
在 LISP Form 中已經提過了,在文件中的說明為 (quote form)
, form
表示不管什麼東西都是可以放到 Quote Special Form 中的,quote 的功能就是不讓這些 form 被 eval。
以下這個 s-exp 會被當成一個 List:
(quote (* 17 (+ 8 9)))
不管什麼東西都可以被 quote:
(quote 🙂)
6. fn
fn 其實就是 Clojure 的 lambda function,寫法為:
(fn name? [params] exprs) (fn name? ([params] exprs) +) 其中 fn 的 name 不是為了給其它 s-exp 使用的名稱,它是為了做 recursive 的名稱。 e.g.
(fn [] "I'm no one!")
(fn fn-name [para1]
(str "Hello, " para1 "!"))
(fn-name "peter") ; <- 會跳 exception
; 找不到 fn-name 這個 function
;; 用 fn 定義一個 function, 加入參數 name (命名 power)
(fn power [n e]
(if (zero? e)
1
(* n (power n (dec e)))))
fn 外會找到名叫 "power" 的 function 理由是 fn 的 name? 參數是用來給 lambda 內做 recursive 用的 (power 2 10) ; 找不到 power 這個 function
如果想要用 lambda function,就是再加一層括號,並加上 function parameter。 ((fn ,,, ,,,) ,,, ,,, )
^ ^ ^
1st exp 後面的 exp 會被 eval 後做為 1st exp 的 parameter 第一個 s-exp 就算是 lambda function 也依然符合 LISP form 的 Eval 規則 ((fn power [n e] (if (zero? e) 1
(* n (power n (dec e))))) 2 10)
TODO def , defn (defn function-symbol [] (function body))Literally Lambda Lambda 有一個寫法為 hash + 小刮號 : #(exp*)
#(,,,)
如果只有一個參數,'%' 用來表示該參數 (#(str "Hello, " %) "Dave")
(#(str "I'm sorry " % ", I'm afraid I can't do that.") "Dave")
;; 如果有多個參數,%N (N表示數字) 能依序代表各參數 (#(list %2 %1 %3) "🙈" "🙉" "🙊")
7. loop & recur
如果有學過其它語言: for ( 起始變數 ; 終止條件 ; 變數變化 ) { }
(loop [起始變數]
(if (終止條件)
回傳值
(recur 變數變化)))
e.g.(loop [n 10]
(if (< n 0)
"BOOM!"
(do
(println n "!")
(Thread/sleep 100)
(recur (dec n)))))
((fn loop1 [n]
(if (< n 0)
"BOOM!"
(do
(println n "!")
(Thread/sleep 100)
(loop1 (dec n))))) 10)
loop 如果沒有 recur (loop [n 10] (println n "!")) ; 結果是不會重覆執行
; 也就是說 loop 一定要和 recur 一起使用
What Macro ?
Flow Control
Side Effect
重新回顧 Clojure Verbs:LISP Form、Special Form、Macro
結語
到這邊為止,相信讀者應該對 Clojure 有基本的認識了,但要拿 Clojure 做一些有用的東西,還缺少了寫程式的環境,和管理專專的工具。
在自己的電腦安裝 Clojure 的環境請參考:
編輯器的部份請參考:
有關 Clojure 進階的功能請參考:
Code Block 的小袐密
其實本文中用到的能處理 Clojure 語法的 Code Block 並不是 Clojure 而是 ClojureScript, ClojureScript 是一個在 JavaScript 環境上跑的 Clojure,但基本語法和 Clojure 相同的, 不影響本文中 Clojure 的範例結果。
會一個 Clojure 語法就可以寫前後端,那當然會有 ClojureScript 的教學啦: