#:person{:first "Han"
:last "Solo"
:ship #:ship{:name "Millennium Falcon"
:model "YT-1300f light freighter"}}
Clojure 是一種 同像性 語言,這是一個花俏的術語,用來描述 Clojure 程式由 Clojure 資料結構表示的事實。這是 Clojure(和 Common Lisp)與大多數其他程式語言之間一個非常重要的區別 - Clojure 是根據資料結構的評估來定義的,而不是根據字元串流/檔案的語法來定義的。對於 Clojure 程式來說,操作、轉換和產生其他 Clojure 程式是很常見且容易的。
話雖如此,大多數 Clojure 程式都是從文字檔案開始的,而讀取器的任務就是分析文字並產生編譯器將看到的資料結構。這不只是編譯器的一個階段。讀取器和 Clojure 資料表示本身在許多與 XML 或 JSON 等相同的環境中都有用處。
可以說讀取器具有以字元定義的語法,而 Clojure 語言具有以符號、清單、向量、映射等定義的語法。讀取器由函式 read 表示,它會從串流中讀取下一個形式(不是字元),並傳回該形式所表示的物件。
由於我們必須從某處開始,因此此參考從評估開始的地方開始,從讀取器形式開始。這不可避免地需要討論資料結構,其描述性細節和編譯器的解釋將隨後說明。
符號以非數字字元開頭,可以包含字母數字字元和 *, +, !, -, _, ', ?, <, > 和 =(最終可能會允許其他字元)。
'/' 具有特殊意義,它可以在符號中間使用一次,以將名稱空間與名稱分開,例如 my-namespace/foo
。'/' 本身命名為除法函式。
「.」具有特殊意義 - 它可以在符號中間使用一次或多次,以指定完全限定的類別名稱,例如 java.util.BitSet
,或在命名空間名稱中。以「.」開頭或結尾的符號由 Clojure 保留。包含 / 或 . 的符號稱為「限定」。
以「:」開頭或結尾的符號由 Clojure 保留。符號可以包含一個或多個不重複的「:」。
字串 - 以「雙引號」括住。可以跨多行。支援標準 Java 逸出字元。
數字 - 通常按照 Java 的方式表示
整數可以無限長,在範圍內時會讀取為 Long,否則讀取為 clojure.lang.BigInt。帶有 N 後綴的整數始終讀取為 BigInt。八進制表示法允許使用 0
前綴,十六進制表示法允許使用 0x
前綴。如果可能,可以使用 2 到 36 的基數指定它們(請參閱 Long.parseLong());例如 2r101010
、052
、8r52
、0x2a
、36r16
和 42
都是相同的 Long。
浮點數讀取為 Double;帶有 M 後綴時,它們讀取為 BigDecimal。
支援比率,例如 22/7
。
字元 - 前面加上反斜線:\c
。\newline
、\space
、\tab
、\formfeed
、\backspace
和 \return
產生對應的字元。Unicode 字元使用 \uNNNN
表示,如 Java 中所示。八進制使用 \oNNN
表示。
nil
表示「無/無值」- 表示 Java null 並測試邏輯假
布林值 - true
和 false
符號值 - ##Inf
、##-Inf
和 ##NaN
關鍵字 - 關鍵字類似於符號,但
它們可以且必須以冒號開頭,例如::fred。
它們不能在名稱部分中包含「.」或命名類別。
與符號一樣,它們可以包含命名空間 :person/name
,其中可能包含「.」。
以兩個冒號開頭的關鍵字會在當前命名空間中自動解析為限定關鍵字
如果關鍵字未限定,命名空間將為當前命名空間。在 user
中,::rect
讀取為 :user/rect
。
如果關鍵字限定,將使用當前命名空間中的別名解析命名空間。在將 x
別名為 example
的命名空間中,::x/foo
解析為 :example/foo
。
映射是零個或多個以大括號括起來的鍵/值對:{:a 1 :b 2}
逗號被視為空白,可用於組織對:{:a 1, :b 2}
鍵和值可以是任何形式。
Clojure 1.9 中新增
映射文字可以選擇使用 #:ns
前綴指定映射中鍵的預設命名空間內容,其中 ns 是命名空間的名稱,而前綴出現在映射的開頭大括號 {
之前。此外,#::
可用於自動解析命名空間,其語義與自動解析關鍵字相同。
具有命名空間語法的映射文字與沒有命名空間語法的映射文字有以下不同的讀取方式
鍵
沒有命名空間的關鍵字或符號鍵會使用預設命名空間讀取。
有命名空間的關鍵字或符號鍵不會受到影響,但特殊命名空間 _
除外,它會在讀取期間移除。這允許在具有命名空間語法的映射文字中指定沒有命名空間的關鍵字或符號作為鍵。
不是符號或關鍵字的鍵不會受到影響。
值
值不會受到影響。
巢狀映射文字鍵不會受到影響。
例如,以下具有命名空間語法的映射文字
#:person{:first "Han"
:last "Solo"
:ship #:ship{:name "Millennium Falcon"
:model "YT-1300f light freighter"}}
讀取為
{:person/first "Han"
:person/last "Solo"
:person/ship {:ship/name "Millennium Falcon"
:ship/model "YT-1300f light freighter"}}
呼叫 Java 類別、deftype 和 defrecord 建構函式可以使用其完全限定的類別名稱,其前綴為 # 後接向量:#my.klass_or_type_or_record[:a :b :c]
向量部分中的元素會未經評估傳遞給相關建構函式。defrecord 執行個體也可以使用類似形式建立,該形式採用映射:#my.record{:a 1, :b 2}
映射中的鍵值會未經評估指定給 defrecord 中相關欄位。任何沒有對應文字映射條目的 defrecord 欄位會指定為 nil 作為其值。映射文字中的任何額外鍵值會新增到結果的 defrecord 執行個體。
讀取器的行為是由內建結構與稱為讀取表的擴充系統共同驅動。讀取表中的項目提供從特定字元(稱為巨集字元)到特定讀取行為(稱為讀取器巨集)的對應。除非另有說明,否則巨集字元不能用於使用者符號。
如上所述,產生字元文字。範例字元文字為:\a \b \c
。
下列特殊字元文字可用於常見字元:\newline
、\space
、\tab
、\formfeed
、\backspace
和 \return
。
Unicode 支援遵循 Java 慣例,對應於基礎 Java 版本。Unicode 文字格式為 \uNNNN
,例如 \u03A9
是 Ω 的文字。
元資料是與某些類型的物件相關聯的對應:符號、清單、向量、集合、對應、傳回 IMeta 的標記文字,以及記錄、類型和建構函式呼叫。元資料讀取器巨集會先讀取元資料,然後將其附加到下一個讀取的表單(請參閱 with-meta 以將元資料附加到物件)
^{:a 1 :b 2} [1 2 3]
產生向量 [1 2 3]
,其元資料對應為 {:a 1 :b 2}
。
簡寫版本允許元資料為簡單的符號或字串,這種情況下,它會被視為單一項目對應,其鍵為 :tag,值為(已解析的)符號或字串,例如
^String x
等同於 ^{:tag java.lang.String} x
此類標記可用於傳達類型資訊給編譯器。
另一個速記版本允許元資料成為關鍵字,這種情況下它會被視為單一輸入的 map,其鍵為關鍵字,值為 true,例如:
^:dynamic x
等於 ^{:dynamic true} x
元資料可以串聯,這種情況下它們會從右至左合併。
dispatch 巨集會導致讀取器使用另一個表格的讀取器巨集,其索引為後面的字元
#{} - 請參閱上方的集合
正規表示式模式 (#"pattern")
正規表示式模式會在讀取時讀取並 *編譯*。產生的物件類型為 java.util.regex.Pattern。正規表示式字串不遵循與字串相同的跳脫字元規則。特別是,模式中的反斜線會被視為本身(不需要額外反斜線跳脫)。例如,(re-pattern "\\s*\\d+")
可以更簡潔地寫成 #"\s*\d+"
。
變數引號 (#')
#'x
⇒ (var x)
匿名函式文字 (#())
#(…)
⇒ (fn [args] (…))
其中 args 由採用 %, %n 或 %& 形式的引數文字的存在決定。% 是 %1 的同義詞,%n 指定第 n 個引數(從 1 開始),而 %& 指定剩餘引數。這並非 fn 的替代品 - 慣用語法會用於非常簡短的一次性對應/篩選函式等。#() 形式不可巢狀。
忽略下一個形式 (#_)
讀取器會完全略過 #_ 後面的形式。(這比會產生 nil 的 comment 巨集更徹底地移除。)
對於符號、清單、向量、集合和映射以外的所有形式,`x 與 'x 相同。
對於符號,語法引號會在目前的內容中 *解析* 符號,產生完全限定的符號(即命名空間/名稱或完全.限定.類別名稱)。如果符號沒有命名空間限定且以「#」結尾,它會解析為同名的已產生符號,其後附加「_」和唯一的 ID。例如,x# 會解析為 x_123。語法引號表達式中對該符號的所有參考都會解析為同一個已產生的符號。
對於清單/向量/集合/映射,語法引號會建立對應資料結構的範本。在範本中,未限定的形式會表現得好像遞迴地語法引號,但可以使用取消引號或取消引號展開來豁免這些形式的遞迴引號,這種情況下它們會被視為表達式,並分別以其值或值序列取代範本中。
例如
user=> (def x 5)
user=> (def lst '(a b c))
user=> `(fred x ~x lst ~@lst 7 8 :nine)
(user/fred user/x 5 user/lst a b c 7 8 :nine)
目前使用者程式無法存取讀取表格。
Clojure 的讀取器支援 可延伸資料標記法 (edn) 的超集。edn 規格正在積極開發中,並透過以語言中立的方式定義 Clojure 資料語法的子集,來補充本文件。
標記文字是 Clojure 對 edn 標記元素 的實作。
當 Clojure 啟動時,它會在類別路徑的根目錄搜尋名為 data_readers.clj
或 data_readers.cljc
的檔案。每個此類檔案都必須包含一個 Clojure 符號對應表,如下所示
{foo/bar my.project.foo/bar
foo/baz my.project/baz}
每個配對中的金鑰都是 Clojure 讀取器會辨識的標記。配對中的值是 變數 的完整限定名稱,讀取器會呼叫該變數來剖析標記後的形式。例如,根據上述的 data_readers.clj
檔案,Clojure 讀取器會剖析此形式
#foo/bar [1 2 3]
透過在向量 [1 2 3]
上呼叫變數 #'my.project.foo/bar
來執行。資料讀取器函式會在讀取器將形式讀取為正常的 Clojure 資料結構後,在該形式上執行。對於您自己的資料讀取器函式,您應該透過擲出具有提供錯誤資訊訊息的執行時期例外狀況實例來報告錯誤。
沒有命名空間限定詞的讀取器標記是保留給 Clojure 使用的。預設讀取器標記定義在 default-data-readers 中,但可以在 data_readers.clj
/ data_readers.cljc
中覆寫,或透過重新繫結 *data-readers* 來覆寫。如果找不到標記的資料讀取器,繫結在 *default-data-reader-fn* 中的函式會使用標記和值來產生值。如果 *default-data-reader-fn* 為 nil (預設值),將會擲出執行時期例外狀況。
如果提供了 data_readers.cljc
,它會使用與任何其他具有讀取器條件式的 cljc 來源檔案相同的語意來讀取。
Clojure 1.4 導入了 instant 和 UUID 標記文字。時間戳記的格式為 #inst "yyyy-mm-ddThh:mm:ss.fff+hh:mm"
。注意:此格式中有些元素是可選的。詳細資訊請參閱程式碼。預設讀取器會將提供的字串解析為 java.util.Date
。例如
(def instant #inst "2018-03-28T10:48:00.000")
(= java.util.Date (class instant))
;=> true
由於 *data-readers* 是可繫結的動態變數,因此您可以將預設讀取器替換為其他讀取器。例如,clojure.instant/read-instant-calendar
會將文字解析為 java.util.Calendar
,而 clojure.instant/read-instant-timestamp
會將文字解析為 java.util.Timestamp
(binding [*data-readers* {'inst read-instant-calendar}]
(= java.util.Calendar (class (read-string (pr-str instant)))))
;=> true
(binding [*data-readers* {'inst read-instant-timestamp}]
(= java.util.Timestamp (class (read-string (pr-str instant)))))
;=> true
#uuid
標記文字會解析為 java.util.UUID
(= java.util.UUID (class (read-string "#uuid \"3b8a31ed-fd89-4f1b-a00f-42e3d60cf5ce\"")))
;=> true
讀取標記文字時,如果找不到資料讀取器,就會呼叫 *default-data-reader-fn*。您可以設定自己的預設資料讀取器函式,並使用提供的 tagged-literal 函式建立可儲存未處理文字的物件。tagged-literal
傳回的物件支援關鍵字查詢 :tag
和 :form
(set! *default-data-reader-fn* tagged-literal)
;; read #object as a generic TaggedLiteral object
(def x #object[clojure.lang.Namespace 0x23bff419 "user"])
[(:tag x) (:form x)]
;=> [object [clojure.lang.Namespace 599782425 "user"]]
Clojure 1.7 針對可由多個 Clojure 平台載入的攜帶式檔案,推出了新的副檔名 (.cljc)。管理平台特定程式碼的主要機制,是將該程式碼隔離到最少數量的命名空間中,然後提供這些命名空間的平台特定版本 (.clj/.class 或 .cljs)。
在無法隔離程式碼中變動部分的情況下,或當程式碼大部分可攜帶,只有少數平台特定部分時,1.7 也推出了 讀取器條件,僅支援 cljc 檔案和預設 REPL。應謹慎使用讀取器條件,且僅在必要時使用。
讀取條件式是一種新的讀取分派形式,以 #?
或 #?@
開頭。兩者都包含一系列交替特徵和表達式,類似於 cond
。每個 Clojure 平台都有眾所周知的「平台特徵」- :clj
、:cljs
、:cljr
。讀取條件式中的每個條件都會依序檢查,直到找到與平台特徵相符的特徵。讀取條件式會讀取並傳回該特徵的表達式。每個未選取分支上的表達式都會被讀取但略過。眾所周知的 :default
特徵將永遠相符,可用於提供預設值。如果沒有分支相符,則不會讀取任何形式(就像沒有讀取條件式表達式一樣)。
非官方 Clojure 平台的實作人員應使用限定關鍵字作為其平台特徵,以避免名稱衝突。非限定平台特徵保留給官方平台。 |
以下範例將在 Clojure 中讀取為 Double/NaN,在 ClojureScript 中讀取為 js/NaN,在任何其他平台中讀取為 nil
#?(:clj Double/NaN
:cljs js/NaN
:default nil)
#?@
的語法完全相同,但預期表達式會傳回一個集合,該集合可以拼接進周圍的內容,類似於語法引號中的取消引號拼接。不支援在頂層使用讀取條件式拼接,並且會擲回例外。一個範例
[1 2 #?@(:clj [3 4] :cljs [5 6])]
;; in clj => [1 2 3 4]
;; in cljs => [1 2 5 6]
;; anywhere else => [1 2]
read 和 read-string 函數可以選擇將選項地圖作為第一個引數。目前的特徵組和讀取條件式行為可以用這些鍵和值設定在選項地圖中
:read-cond - :allow to process reader conditionals, or
:preserve to keep all branches
:features - persistent set of feature keywords that are active
如何從 Clojure 測試 ClojureScript 讀取條件式的範例
(read-string
{:read-cond :allow
:features #{:cljs}}
"#?(:cljs :works! :default :boo)")
;; :works!
不過,請注意 Clojure 讀取器永遠也會注入平台特徵 :clj。對於與平台無關的讀取,請參閱 tools.reader。
如果讀取器以 {:read-cond :preserve}
呼叫,則讀取條件式和未執行的分支將會保留在傳回的形式中,作為資料。讀取條件式將會傳回一個類型,支援使用 :form
鍵和 :splicing?
旗標的關鍵字擷取。已讀取但略過的標記文字將會傳回一個類型,支援使用 :form
和 :tag
鍵的關鍵字擷取。
(read-string
{:read-cond :preserve}
"[1 2 #?@(:clj [3 4] :cljs [5 6])]")
;; [1 2 #?@(:clj [3 4] :cljs [5 6])]
下列函數也可以用作這些類型的謂詞或建構函數
reader-conditional? reader-conditional tagged-literal? tagged-literal