你就要開始讀伊塔羅•卡爾維諾的新小說《如果在冬夜,一個旅人》。
— 卡爾維諾《如果在冬夜,一個旅人》
Clojure 程式開始於一串文字,經由讀取器 (Reader) 把程式轉化成資料結構;詮釋資料 (Metadata) 則是描述資料的資料,或者可以稱作元資料或中介資料。本篇文章將介紹讀取器以及詮釋資料的相關知識。
Clojure 程式的生命由一串文字開始,讀取器 (Reader) 將文字解析之後,產生出編譯器 (Compiler) 可以認識的資料結構。
讀取器嘗試將文字解析成形式 (Form) 或稱作運算式 (Expression),而形式 (Form) 是指任何可以順利被求值 (Evaluation) 的合法程式單元。
任何可以順利被求值的程式單元,包括下列幾種:
任何以非數字開頭的文字皆是符號,符號名稱可以有四則運算字符以及問號,它的型態爲 clojure.lang.Symbol。
常值 (Literal) 指的是程式中代表固定值的連續字符。Clojure 中的常值共有以下幾種:
-
字串 (String)
任何以雙引號 (") 包覆的字符會被看成字串,它的型態跟 Java 中的字串一樣,皆是 java.lang.String。
-
數字 (Number)
以數字字符開頭的連續字符,共有整數 (Integer)、浮點數 (Float) 以及有理數 (Ratio)。型態分別爲 java.lang.Long、clojure.lang.BigInt、java.lang.Double 以及 clojure.lang.Ratio。
-
字符 (Character)
字符以反斜線 (\) 開頭,與實際的字符相對應。型態爲 java.lang.Character。
-
nil
代表虛無與不存在,與 Java 中的 null 相同意思。
-
布林 (Boolean)
由
true
與false
代表邏輯上的真與假。型態爲 java.lang.Boolean。 -
關鍵字 (Keyword)
由冒號 (:) 開頭的連續字符被當作關鍵字,與符號類似,大半用作索引值。型態爲 clojure.lang.Keyword。
以左右小括號 (()
) 圍起,內部可以是任何形式 (Form)。型態爲 clojure.lang.PersistentList。
以左右中括號 ([]
) 圍起,內部可以是任何的形式 (Form)。型態爲 clojure.lang.PersistentVector。
以左右大括號 ({}
) 圍起,內部是索引與值的對應關係,稱爲向量。索引與值可以是任何的形式 (Form)。型態爲 clojure.lang.PersistentHashMap、clojure.lang.PersistentArrayMap 或 clojure.lang.PersistentTreeMap。
以大括弧 ({}
) 圍起任何形式,並在前面加上井號 (#) 被當作集合。型態爲 clojure.lang.PersistentHashSet 或 clojure.lang.PersistentTreeSet。
有一些字符經由讀取器解析時,會執行特殊的行爲,這些字符被稱爲讀取巨集 (Reader macro)。在 LISP 程式語言中,除了內建的讀取巨集之外,使用者還可以自定讀取巨集,用以改變讀取器的行爲。而在 Clojure 中,讀取巨集則無法讓使用者自行訂製。
以下列出 Clojure 中會被視爲讀取巨集的各種字符:
如果單引號 (') 放置在任何符號前面,將會抑制 Clojure 對符號求值,將該符號原封不動返回,這種行爲稱爲引用 (Quote)。 與使用 quote
函式功能一樣。
而遇到反斜線 (\) 時,讀取器則會將它其後字符返回,成爲字符常值 (Character literal)。
解析到分號 (;) 則會將其後的字符忽略不做解析,是爲註解 (Comment)。
小老鼠符號則是會呼叫 deref
函式,取出其後的參數所引導的值,稱爲標的 (De-reference)。用在取出參考類型所儲存的值,或是等待由 promise
與 future
函式產生的延遲運算,計算完畢返回。
插入符號 (^) 會伴隨着一個映射,其中是一些對於映射之後物件的描述資訊,這些資訊稱作詮釋資料 (Metadata)。與函式 with-meta
的功能一樣。
之後的小節將會有詮釋資料的詳細介紹。
根據井字符號 (#) 之後的字符,讀取器會有不同的行爲,所以井字符號被稱作發派 (Dispatch) 巨集。以下是與井字符號搭配的各字符說明:
-
#{}
集合。
-
#""
正則表達式。
-
#'
傳回之後符號所代表的 Var 物件,與
var
函式相同。 -
#()
匿名函式。
-
#_
之後的形式將會被讀取器忽略。
反引號 (`) (位置在鍵盤按鍵 1 左邊) 被稱爲語法引用 (Syntax quote),是 Clojure 巨集中使用的特殊符號之一,用來產生文字範本 (Template),範本中的形式將不會被求值。後續的章節將會有詳細的介紹。
波浪號 (~) 被稱爲解引用 (Unquote),使用在反引號建立的文字範本內,讓波浪號後面跟隨的符號跳出範本而求值。
若波浪號 (~) 之後是小老鼠符號 (@),則被稱爲解引用拼接 (Unquote splice)。它的功用是將範本中的列表解消,替換成列表中的各個元素。
詮釋資料是添加在符號或群集中的映射,其中記載了該符號或群集的資訊。使用 with-meta
函式添加詮釋資料,它將返回添加了資料的物件;或用 meta
函式取得詮釋資料:
(with-meta [1 2 3] {:trivial true})
;; => [1 2 3]
(meta (with-meta [1 2 3] {:trivial true}))
;; => {:trivial true}
或使用更簡便的方式,在映射前面加上插入符號 (^) 添加詮釋資料:
(def user ^{:birth "12-21"} {:name "Catherine"})
user
;; => {:name "Catherine"}
(meta user)
;; => {:birth "12-21"}
如果詮釋資料的映射中只有一個索引鍵與值的對應,而且值的內容爲真,則可以如以下的簡寫:
(def ^{:private true} x [1 2 3])
(def ^:private y [1 2 3])
以上的兩個符號都添加了私有的資訊,在其他的命名空間中無法取用。
若是用 def
或 defn
定義符號與 Var 物件時,在符號前面寫下詮釋資料,則詮釋資料將會被用在 Var 物件而不是符號,所以查看函式的詮釋資料必須查看儲存函式的 Var 物件,而不是符號:
(def ^{:doc "Nothing special"} x [1 2 3])
(meta x)
;; => nil
(meta (var x))
;; => {:doc "Nothing special", :line 1, :column 1, :file "/private/var/folders/5n/sm_s13cn3lb_p_4n2khqd0mr0000gn/T/form-init6817815229097680482.clj", :name x, :ns #namespace[user]}
函式的說明文件也是利用詮釋資料的方式,添加到儲存函式的 Var 物件上。Var 物件的 :doc
索引鍵對應的值便是該物件的說明文件:
(defn doublex "Double the param" [x] (* x x))
(meta #'doublex)
;; => {:arglists ([x]), :doc "Double the param", :line 1, :column 1, :file "/private/var/folders/5n/sm_s13cn3lb_p_4n2khqd0mr0000gn/T/form-init6817815229097680482.clj", :name doublex, :ns #namespace[user]}
從以上的範例可以看到,我們使用 meta
取得儲存函式 doublex
的 Var 物件的詮釋資料,其中的索引鍵 doc
便存放著定義函式時寫下的說明文件。
Clojure 的核心函式也攜帶了豐富的詮釋資料,其中有該函式的命名空間、Var 物件的名稱、參數列表、說明文件、該函式何時加入 Clojure 等等的資訊。以下是 str
函式的詮釋資料:
(meta #'str)
;; => {:added "1.0", :ns #namespace[clojure.core], :name str, :file "clojure/core.clj", :static true, :column 1, :line 533, :tag java.lang.String, :arglists ([] [x] [x & ys]), :doc "With no args, returns the empty string. With one arg x, returns\n x.toString(). (str nil) returns the empty string. With more than\n one arg, returns the concatenation of the str values of the args."}
讀取器將一般文字轉換成一連串的形式之後,交給編譯器 (Compiler) 編譯成 Java 虛擬機位元碼,其中有一些形式的求值方法不同於一般的形式,稱爲特殊形式 (Special forms)。
舉例來說,前面章節提到過的 if
形式是一種特殊形式,它不像一般形式會在呼叫之前,先將各個參數求值,而是依據條件式的真與假,才決定對哪一個分支繼續求值。
特殊形式是 Clojure 程式語言的基石,所有的東西都是藉由特殊形式而打造出來。以下介紹各種特殊形式:
def
根據給予的符號名稱與資料,建立全域的 Var 物件。
(def a 10)
對 if
的第一個參數求值,若爲真則求值第二個運算式,否而且有第三個運算式則求值。
(if (= a 10) "true" "false")
;; => "true"
依序對 do
其後的各個運算式求值,並返回最後一個運算式求值的結果。
(do
(println "Do")
(str "Something:" 42)
"else")
;; => Do
;; => "else"
以第一個參數向量中的符號與資料建立區域繫結,並對之後的運算式求值,區域繫結只在這些運算式有效。
(let [x 1
y 2]
y)
;; => 2
不對其後的形式求值,原封不動地返回。
(quote (a 1 2))
;; => (a 1 2)
Clojure 不會試圖去尋找以 a
爲名的函式並以參數呼叫,而是照實地返回。
fn
建立函式,函式名稱是否提供都是可選的,之後是以類似 let
的向量參數繫結,參數之後則是函式的本體。
Clojure 的函式實作了 Java 中的 Callable
、Runnable
與 Comparator
三種介面。
(def triplex
(fn this [x]
(* x x x)))
(triplex 3)
;; => 27
與 let
一樣,差別在於建立了遞迴點 (Recursion point) 與 recur
搭配使用,用來反覆循環其中的運算式。
對跟隨在 recur
之後的各參數求值,以新值返回遞迴點重新執行。遞迴點可以藉由 loop
或 fn
建立。
你可以把 loop/recur
視爲顯式 (Explicit) 的尾遞迴 (Tail recursion)。
(def fib
(fn [x]
(loop [a 0 b 1 cnt x]
(if (= cnt 0)
a
(recur (+' a b) a (dec cnt))))))
(fib 10)
;; => 55
求值其後的運算式,並將得到的例外拋出。
(throw (Exception. "my exception message"))
;; => Exception my exception message
try
有三個參數,第一個參數爲運算式本體,先對此運算式求值之後,若拋出例外且符合 catch
欲捕捉的例外,則執行 catch
的本體運算式。而不管是否有例外發生,finally
的本體運算式都會被求值。
經由本篇文章,你了解了什麼是讀取器以及讀取巨集,還有讀取巨集中各個字符代表的特殊功能;還知道了詮釋資料的用途,比如添加或取得詮釋資料。更了解了奠定 Clojure 基礎的各種特殊形式。
還不賴吧?今天就先到這裡,下一篇文章再見囉!