跳至內容

學習 Clojure/資料型別

來自華夏公益教科書

關於 Clojure 的所有型別,有幾點值得注意的事情

  1. Clojure 是用 Java 實現的:編譯器是用 Java 編寫的,Clojure 程式碼本身作為 Java VM 程式碼執行。因此,Clojure 中的資料型別是 Java 資料型別:Clojure 中的所有值都是普通的 Java 引用物件, Java 類的例項。
  2. 大多數 Clojure 型別是不可變的 一旦建立,它們永遠不會改變。
  3. Clojure 更傾向於使用相等性比較而不是標識比較:例如,比較兩個列表以檢視它們是否是記憶體中的同一個物件,Clojure 的方式是比較它們實際的值, 它們的內容。大多數語言(包括 Java)不這樣做,因為檢查深度結構化物件的值代價高昂,但 Clojure 使其變得廉價:當建立一個 Clojure 物件時,它會保留一個自身的雜湊值,在相等性比較中比較的是這個雜湊值,而不是實際檢查物件;只要比較的結構完全不可變,這個雜湊值就足夠了。(注意儲存在不可變 Clojure 集合物件中的可變 Java 物件的情況。如果可變物件發生改變,這不會反映在集合的雜湊值中。)

Java 為其原始數字型別包括包裝引用型別,例如 java.lang.Integer "封裝"(包裝)原始 int 型別。因為每個 Clojure 函式都是一個 JVM 方法,它期望 Object 引數,所以 Java 原語通常在 Clojure 函式中被封裝:當 Clojure 呼叫一個 Java 方法時,返回的原語會自動包裝,並且傳遞給 Java 方法的任何引數會根據需要自動解包。(但是,型別提示允許 Clojure 函式中的非引數區域性變數成為解包的原語,這在你嘗試最佳化迴圈時很有用。)

Clojure 使用 java.lang.BigIntjava.lang.BigDecimal 類分別用於任意精度的整數和小數值。Clojure 算術運算的特殊版本(+'、-'、*'、inc' 和 dec')會根據需要智慧地返回這些型別的值,以確保結果始終完全精確。

一些有理數 simply cannot be represented in floating-point, 所以 Clojure 添加了一個 Ratio 型別。Ratio 值是兩個整數之間的比率。寫成文字,Ratio 是兩個整數之間用斜槓分隔,例如 23/55(二十三五十五分之二十三)。

Clojure 算術運算會根據需要智慧地返回整數或比率,例如 7/3 加 2/3 返回 3,11 除以 5 返回 11/5。只要你的計算只涉及整數和比率,結果將是數學上完全準確的,但一旦浮點數或 BigDecimal 值進入混合,你將獲得浮點數或 BigDecimal 結果,這可能會導致結果是數學上完全準確的,例如 1 除以 7 返回 1/7,但 1 除以 7.0 返回 0.14285714285714285。

Clojure 中的字串僅僅是 java.lang.String 的例項。與 Java 一樣,字串文字用雙引號括起來,但與 Java 不同的是,字串文字可以跨越多行。

java.lang.Character 文字寫成 \ 後面跟著字元

\e
\t
\tab
\newline
\space

如你所見,空格字元寫成 \ 後的單詞。

布林值

[編輯 | 編輯原始碼]

文字 truefalse 分別表示值 java.lang.Boolean.TRUEjava.lang.Boolean.FALSE

在大多數 Lisp 方言中,有一個值與 Java null 類似,叫做 nil。在 Clojure 中,nil 就是 Java 的 null 值,故事到此結束。

在 Java 中,只有 truefalse 是條件表示式的合法值,但在 Clojure 中,條件表示式將 nil 視為具有false 真值。因此,雖然 !null(“非空”)是無效的 Java,但 Clojure 等價物 (not nil) 返回 true

Clojure 中的函式是一種物件型別,因此 Clojure 函式不僅可以被呼叫,還可以作為引數傳遞。由於 Clojure 是一種動態語言,Clojure 函式引數沒有型別——任何型別的引數都可以傳遞給任何 Clojure 函式——但 Clojure 函式有一個設定的arity,因此如果你傳遞給函式錯誤的數量的引數,就會丟擲異常。但是,函式的最後一個引數可以宣告為接受任何額外的引數作為列表(就像 Java 中的“可變引數”一樣),這樣函式就可以接受n 個或更多引數。

Var 是 Clojure 中為數不多的可變型別之一。Var 基本上是一個用於儲存另一個物件的儲存單元——一個集合,基本上就是一組。

一個單獨的 Var 實際上可以構成多個引用:一個根繫結(對所有執行緒可見的繫結)和任意數量的執行緒區域性繫結(每個繫結對單個執行緒可見)。當訪問 Var 的值時,訪問的繫結可能取決於執行訪問的執行緒:如果 Var 對該執行緒具有執行緒區域性繫結,則返回 Var 的執行緒區域性繫結的值;否則,返回 Var 的根繫結值(如果有)。

通常,Clojure 中的所有全域性函式和變數都儲存在 Var 的根繫結中。因為 Var 是可變的,所以我們可以更改 Var 的值來修改正在執行的系統。例如,我們可以用一個修復後的替換來替換一個有缺陷的函式。這是有效的,因為在 Clojure 中,編譯後的函式繫結到儲存它呼叫的函式的 Var,而不是 函式本身,也不是 用於指定 Var 的名稱;由於 Var 是可變的,因此函式呼叫的函式可以在不重新定義函式的情況下發生更改。

Clojure 中的區域性引數和變數是不可變的:它們在生命週期的開始被繫結,並且之後再也不會被繫結。然而,有時我們確實想要可變區域性變數,而具有執行緒區域性繫結的 Var 可以滿足這一目的。

執行緒區域性繫結還允許我們僅在本地上下文的範圍內進行修改。假設我們有一個函式 cat,它呼叫儲存在 Var 中的函式;如果一個函式 goat 被根繫結到 Var,那麼 cat 通常會呼叫 goat;但是,如果我們在一個範圍中呼叫 cat,在這個範圍中,我們將一個函式 moose 執行緒區域性繫結到該 Var,那麼 cat 將呼叫 moose 而不是 goat

名稱空間

[編輯 | 編輯原始碼]

你應該將程式碼組織到名稱空間中。Clojure 名稱空間是一個表示符號值到 Var 和/或 java.lang.Class 物件的對映的物件。

  • Var 可以引用內聯在名稱空間中:區別在於 Var 只可以內聯在一個名稱空間中,但可以引用在任意數量的名稱空間中。換句話說,Var 被內聯的名稱空間是它“真正”所屬的名稱空間。
  • 類只能引用,不能內聯在名稱空間中。當建立一個名稱空間時,它會自動包含對 java.lang 的類進行引用。

從某種意義上說,名稱空間本身存在於一個全域性名稱空間中:名稱空間名稱對單個名稱空間是唯一的,例如 你永遠不會擁有超過一個名為 foo 的名稱空間。

當 Clojure 啟動時,它會建立一個名為 clojure 的名稱空間,在其中將符號 *ns* 對映到一個 Var,該 Var 用於儲存“當前名稱空間”。然後,Clojure 執行一個名為 core.clj 的指令碼,它在 clojure 中內聯許多標準函式,包括用於操作當前名稱空間的函式,例如

  • in-ns 將當前名稱空間設定為特定名稱空間(直接操作 clojure/*ns* 是不被鼓勵的)。
  • import 將 Class 物件引用到當前名稱空間。
  • refer 將另一個名稱空間的內聯 Var 引用到當前名稱空間。

在 Lisp 中,通常在其他語言中稱為識別符號的東西稱為符號。然而,符號不僅僅是編譯器看到的名稱,而是一種值,一種類似字串的值—— 一系列字元。由於符號是一種值,因此符號可以儲存在集合中,作為引數傳遞給函式,等等,就像任何其他物件一樣。

符號只能包含字母數字字元和 * + ! / . : - _ ?,但不能以數字或冒號開頭

rubber-baby-buggy-bumper!      ; valid
j3_!:7                         ; valid
HELICOPTER                     ; valid 
+fiduciary+                    ; valid
3moose                         ; invalid
rubber baby buggy bumper       ; invalid

包含 / 的符號是名稱空間限定

foo/bar    ; a symbol qualified with the namespace name "foo"

包含 . 的符號在評估時會得到特殊處理,我們將在後面看到。

Clojure 的一個關鍵特性是它的標準集合型別——主要是列表和雜湊對映——都是持久的。持久集合是一個不可變的物件,但從現有集合中生成新的集合是廉價的,因為不需要複製現有資料。例如,將元素追加到持久列表的操作實際上不會修改列表,而是返回一個新的列表,該列表與原始列表相同,但多了一個元素;此新列表的建立成本很低,因為它主要只需要建立一個新節點並將其連結到已存在的列表節點,這些節點現在由兩個列表共享。原始集合和新集合都具有相同的效能特徵。

  • 列表

Clojure 持久列表型別是一個單向連結串列,用括號表示字面量

(53 "moo" asdf)   ; a list of three elements: a number, a string, and a symbol
  • 向量

單向連結串列在效能方面通常不合適,因此 Clojure 包含了一種名為向量的型別。Clojure 向量是一個有序的一維序列,類似於列表,但向量是使用類似雜湊對映的結構實現的,因此索引查詢時間為O(log32 n)而不是O(n)。向量用方括號表示字面量

[53 "moo" asdf]    ; a vector of three elements: a number, a string, and a symbol
  • 雜湊對映

雜湊對映用花括號表示字面量,使得每組兩個引數都是一個鍵值對

{35 "moo" "quack" 21}   ; a hashmap with the key-value pairs 35 -> "moo" and "quack" -> 21
  • 序列

序列不是實際的集合型別,而是列表、向量、雜湊對映以及所有其他 Clojure 集合型別都符合的介面。序列支援操作firstrestfirst檢索集合的第一個項,而rest檢索所有剩餘項的序列。正如我們將看到的,序列支援大量建立在這兩個基本操作之上的操作。

(當從對映生成序列時,first表示將對映中的單個對作為向量檢索;返回的對在程式設計師看來實際上是隨機的。對映序列的rest是所有剩餘對作為向量的序列。)

關鍵字

[編輯 | 編輯原始碼]

關鍵字是符號的一種變體,其特點是在前面加上冒號

:rubber-baby-buggy-bumper!      ; valid
:j3_!:7                         ; valid
:HELICOPTER                     ; valid 
:+fiduciary+                    ; valid

關鍵字的存在僅僅是因為,正如您將看到的,在程式碼中使用符號類似但實際上不是符號的名稱很有用。預設情況下,關鍵字沒有名稱空間限定。但是,在某些情況下,生成一個有名稱空間限定的關鍵字以避免與其他程式碼的命名衝突可能很有用。為此,可以顯式地限定名稱空間或鍵入由兩個冒號字首的符號

::gina     ; equivalent to :adam/gina (assuming this is in the namespace "adam")

;; in the REPL, after (in-ns 'adam) and (clojure.core/refer 'clojure.core)
adam=> (namespace :gina)        ; no namespace
nil
adam=> (namespace ::gina)
"adam" 
adam=> (namespace :adam/gina) 
"adam"

注意:程式生成的關鍵字與名稱空間有關,存在一個注意事項。可以生成一個看起來屬於名稱空間的關鍵字,但(namespace)將返回nil

; use (namespace) to see what the namespace of the returned keywords is
user=> (keyword "test")        ; a keyword with no namespace
:test
user=> (keyword "user" "test") ; a keyword in the user namespace
:user/test
user=> (keyword "user/test")   ; a keyword that has no namespace but looks like it does!
:user/test

元資料

[編輯 | 編輯原始碼]

元資料是描述其他資料的 data。Clojure 物件可以將一個其他物件(任何實現IPersistentMap的物件)作為元資料附加到它,例如向量可以將一個雜湊對映作為元資料附加到它。

將元資料附加到物件不會修改物件,而是建立一個新物件——實際上,具有不同元資料的物件是不同的物件。但是,相等性比較會忽略元資料。


Previous page
基本操作
學習 Clojure Next page
資料結構
資料型別
華夏公益教科書