跳轉到內容

Clojure 程式設計/概念

來自 Wikibooks,開放世界的開放書籍
數字型別
[編輯 | 編輯原始碼]

Clojure 支援以下數字型別

  • 整數
  • 浮點數
  • 比率
  • 十進位制

Clojure 中的數字 基於 java.lang.Number。BigInteger 和 BigDecimal 受支援,因此我們在 Clojure 中具有任意精度的數字。

Ratio 型別在 Clojure 頁面上描述

比率
表示整數之間的比率。不能約化為整數的整數的除法會產生一個比率,即 22/7 = 22/7,而不是一個浮點數或截斷值。

比率允許計算以數字形式保持。這有助於避免長時間計算中的不準確性。

以下是一個小實驗。我們首先嚐試計算 (1/3 * 3/1) 作為浮點數。之後,我們嘗試使用 Ratio 進行相同的計算。

 (def a (/ 1.0 3.0))
 (def b (/ 3.0 1.0))

 (* a b)
 ;; ⇒ 1.0

 (def c (* a a a a a a a a a a)) ; ⇒ #'user/c
 (def d (* b b b b b b b b b b)) ; ⇒ #'user/d

 (* c d)
 ;; ⇒ 0.9999999999999996

我們想要的結果是 1,但上面 (* c d) 的值為 0.9999999999999996。這是由於在建立 c 和 d 時 a 和 b 相乘造成的不準確性。你真的不希望你的工資單中出現這樣的計算 :)

使用比率執行相同的計算:

 (def a1 (/ 1 3))
 (def b1 (/ 3 1))

 (def c (* a1 a1 a1 a1 a1 a1 a1 a1 a1 a1))
 (def d (* b1 b1 b1 b1 b1 b1 b1 b1 b1 b1))

 (* c d)
 ;; ⇒ 1

結果如我們所願為 1

數字輸入格式
[編輯 | 編輯原始碼]

Clojure 支援下面顯示的常用輸入格式

 user=> 10       ; decimal
 10
 user=> 010      ; octal
 8
 user=> 0xff     ; hex
 255
 user=> 1.0e-2   ; double
 0.01
 user=> 1.0e2    ; double
 100.0

為了簡化操作,還支援以 <radix>r<number> 形式的基數輸入格式。其中基數可以是 2 到 36 之間的任何自然數。

 2r1111
 ;; ⇒ 15

這些格式可以混合使用。

 (+ 0x1 2r1 01)
 ;; ⇒ 3

Clojure API 也支援許多位運算。

 (bit-and 2r1100 2r0100)
 ;; ⇒ 4

其他一些是

  • (bit-and x y)
  • (bit-and-not x y)
  • (bit-clear x n)
  • (bit-flip x n)
  • (bit-not x)
  • (bit-or x y)
  • (bit-set x n)
  • (bit-shift-left x n)
  • (bit-shift-right x n)
  • (bit-test x n)
  • (bit-xor x y)

檢視 Clojure API 以獲取完整文件。

將整數轉換為字串
[編輯 | 編輯原始碼]

將任何資料格式化為列印的一種通用方法是使用 java.util.Formatter

預定義的便捷函式 format 使使用 Formatter 變得簡單(%#x 中的雜湊將數字顯示為以 0x 為字首的十六進位制數)

 
 (format "%#x" (bit-and 2r1100 2r0100))
 ;; ⇒ "0x4"

使用 java.lang.Integer 將整數轉換為字串更加容易。請注意,由於方法是靜態的,我們必須使用“/”語法而不是“。method”

 
 (Integer/toBinaryString 10)
 ;; ⇒ "1010"
 (Integer/toHexString 10)
 ;; ⇒ "a"
 (Integer/toOctalString 10)
 ;;⇒ "12"

以下是指定字串表示的基數的另一種方法

 
 (Integer/toString 10 2)
 ;; ⇒"1010"

其中 10 是要轉換的數字,2 是基數。

注意:除了上面用於訪問靜態欄位或方法的語法之外,還可以使用“.”(點)。這是一種特殊形式,用於訪問 Java 中的任意(非私有)欄位或方法,如 Clojure 參考 (Java 互動操作) 中所述。例如

 
 (. Integer toBinaryString 10)
 ;; ⇒ "1010"

對於靜態訪問,建議使用“/”語法。

將字串轉換為整數
[編輯 | 編輯原始碼]

要將字串轉換為整數,我們還可以使用 java.lang.Integer。如下所示。

   user=> (Integer/parseInt "A" 16)      ; hex
   10
   user=> (Integer/parseInt "1010" 2)    ; bin
   10
   user=> (Integer/parseInt "10" 8)      ; oct
   8
   user=> (Integer/parseInt "8")         ; dec
   8

以上各節概述了整數到字串和字串到整數的格式化。Java 庫中有一組非常豐富的文件齊全的函式(這裡不方便全部列出)。這些函式可以輕鬆地用於滿足各種需求。

Clojure 中的結構與 Java 或 C++ 等語言中的結構略有不同。它們也與 Common Lisp 中的結構不同(儘管我們在 Clojure 中有 defstruct)。

在 Clojure 中,結構是對映的特殊情況,並在參考的 資料結構 部分進行了解釋。

其理念是,結構的多個例項需要使用欄位名稱(基本上是對映)訪問其欄位值。這既快速又方便,尤其是在 Clojure 自動將定義為結構例項的訪問器的情況下。

以下是處理結構的重要函式

  • defstruct
  • create-struct
  • struct
  • struct-map

有關完整的 API,請參閱 Clojure 參考的 資料結構 部分。

結構是使用 defstruct 建立的,defstruct 是一個包裝 create-struct 函式的宏,而 create-struct 函式實際上是建立結構的函式。defstruct 使用 create-struct 建立結構,並將其繫結到提供給 defstruct 的結構名稱

create-struct 返回的物件稱為結構基礎。這不是一個結構例項,但包含結構例項應該是什麼樣子的資訊。新的例項是使用 structstruct-map 建立的。

型別為 **關鍵字** 或 **符號** 的結構體欄位名稱可以自動用作函式來訪問結構體的欄位。這是因為結構體是對映,並且此功能由對映支援。 這對於其他型別的欄位名稱(例如字串或數字)**不可行**。由於上述原因,使用關鍵字作為結構體欄位名稱**非常常見**。此外,Clojure 優化了結構體以共享基本鍵資訊。以下是示例用法。

 (defstruct employee :name :id)
 (struct employee "Mr. X" 10)                           ; ⇒ {:name "Mr. X", :id 10}
 (struct-map employee :id 20 :name "Mr. Y")             ; ⇒ {:name "Mr. Y", :id 20}

 (def a (struct-map employee :id 20 :name "Mr. Y"))
 (def b (struct employee "Mr. X" 10))'

 ;; :name and :id are accessors
 (:name a)                                              ; ⇒ "Mr. Y"
 (:id b)                                                ; ⇒ 10
 (b :id)                                                ; ⇒ 10
 (b :name)                                              ; ⇒ "Mr. X"

Clojure 也支援 accessor 函式,該函式可用於獲取欄位的訪問器函式,以方便訪問。當欄位名稱的型別不是關鍵字或符號時,這很重要。這在下面的互動中可以看到。

 (def e-str (struct employee "John" 123))
 e-str
 ;; ⇒ {:name "John", :id 123}

 ("name" e-str) ; ERROR: string not an accessor
 ;; ERROR ⇒
 ;; java.lang.ClassCastException: java.lang.String cannot be cast to clojure.lang.IFn
 ;; java.lang.ClassCastException: java.lang.String cannot be cast to clojure.lang.IFn
 ;;         at user.eval__2537.invoke(Unknown Source)
 ;;         at clojure.lang.Compiler.eval(Compiler.java:3847)
 ;;         at clojure.lang.Repl.main(Repl.java:75)

 (def e-name (accessor employee :name))  ; bind accessor to e-name
 (e-name e-str) ; use accessor
 ;; ⇒ "John"

由於結構體是對映,因此可以使用 assoc 將新欄位新增到結構體例項中。dissoc 可用於刪除這些特定於例項的鍵。但是請注意,**無法**刪除**結構體基本鍵**。

 b
 ;; ⇒ {:name "Mr. X", :id 10}

 (def b1 (assoc b :function "engineer"))
 b1
 ;; ⇒ {:name "Mr. X", :id 10, :function "engineer"}

 (def b2 (dissoc b1 :function)) ; this works as :function is instance
 b2
 ;; ⇒ {:name "Mr. X", :id 10}

 (dissoc b2 :name)  ; this fails. base keys cannot be dissociated
 ;; ERROR ⇒ java.lang.Exception: Can't remove struct key

assoc 也可以用於“更新”結構體。

 a
 ;; ⇒ {:name "Mr. Y", :id 20}

 (assoc a :name "New Name")
 ;; ⇒ {:name "New Name", :id 20}

 a                   ; note that 'a' is immutable and did not change
 ;; ⇒ {:name "Mr. Y", :id 20}

 (def a1 (assoc a :name "Another New Name")) ; bind to a1
 a1
 ;; ⇒ {:name "Another New Name", :id 20}

請注意,與 Clojure 中的其他序列一樣,結構體也是不可變的,因此,簡單地執行上面的 assoc 不會改變 a。因此,我們將它重新繫結到 a1。雖然可以將新值重新繫結回 a,但這**不**被認為是好的風格。

異常處理

[編輯 | 編輯原始碼]

Clojure 支援 基於 Java 的異常。對於習慣於 Common Lisp 條件系統 的 Common Lisp 使用者來說,這可能需要一些適應。

Clojure 不支援條件系統,並且根據 此訊息,短期內也不打算支援。也就是說,Clojure 採用的更常見的異常系統非常適合大多數程式設計需求。

如果您不熟悉異常處理,那麼 Java 異常教程 是學習它們的好地方。

在 Clojure 中,可以使用以下函式來處理異常

  • (try expr* catch-clause* finally-clause?)
    • catch-clause -> (catch classname name expr*)
    • finally-clause -> (finally expr*)
  • (throw expr)

您可能希望在 Clojure 中處理的兩種型別的異常是

  • Clojure 異常:這些是 Clojure 或底層 Java 引擎生成的異常
  • 使用者定義異常:這些是您可能為應用程式建立的異常
Clojure 異常
[編輯 | 編輯原始碼]

下面是在 REPL 中丟擲異常的簡單互動

user=> (/ 1 0)
java.lang.ArithmeticException: Divide by zero
java.lang.ArithmeticException: Divide by zero
        at clojure.lang.Numbers.divide(Numbers.java:142)
        at user.eval__2127.invoke(Unknown Source)
        at clojure.lang.Compiler.eval(Compiler.java:3847)
        at clojure.lang.Repl.main(Repl.java:75)

在上面的情況下,我們看到一個 java.lang.ArithmeticException 被丟擲。這是一個由底層 JVM 丟擲的執行時異常。對於新使用者來說,冗長的訊息有時會令人望而生畏,但訣竅是隻關注異常(java.lang.ArithmeticException: Divide by zero)而不必理會其他跟蹤資訊。

編譯器在 REPL 中也可能會丟擲類似的異常。

user=> (def xx yy)
java.lang.Exception: Unable to resolve symbol: yy in this context
clojure.lang.Compiler$CompilerException: NO_SOURCE_FILE:4: Unable to resolve symbol: yy in this context
        at clojure.lang.Compiler.analyze(Compiler.java:3669)
        at clojure.lang.Compiler.access$200(Compiler.java:37)
        at clojure.lang.Compiler$DefExpr$Parser.parse(Compiler.java:335)
        at clojure.lang.Compiler.analyzeSeq(Compiler.java:3814)
        at clojure.lang.Compiler.analyze(Compiler.java:3654)
        at clojure.lang.Compiler.analyze(Compiler.java:3627)
        at clojure.lang.Compiler.eval(Compiler.java:3851)
        at clojure.lang.Repl.main(Repl.java:75)

在上面的情況下,編譯器找不到 yy 的繫結,因此它丟擲異常。如果您的程式是正確的(即,在這種情況下 yy 已定義為 (def yy 10)),您將不會看到任何編譯時異常。

以下互動顯示瞭如何處理執行時異常,例如 ArithmeticException

user=> (try (/ 1 0)
            (catch Exception e (prn "in catch"))
            (finally (prn "in finally")))
"in catch"
"in finally"
nil

try 塊的語法是 (try expr* catch-clause* finally-clause?)

可以看出,在 Clojure 中處理異常非常容易。需要注意的一點是,(catch Exception e ...) 是對異常的全面捕獲,因為 Exception 是所有異常的超類。也可以捕獲**特定**的異常,這通常是一個好主意。

在下面的示例中,我們專門捕獲 ArithmeticException

user=> (try (/ 1 0) (catch ArithmeticException e (prn "in catch")) (finally (prn "in finally")))
"in catch"
"in finally"
nil

當我們在 catch 塊中使用其他異常型別時,我們發現 ArithmeticException 沒有被捕獲,而是被 REPL 看到。

user=> (try (/ 1 0) (catch IllegalArgumentException e (prn "in catch")) (finally (prn "in finally")))
"in finally"
java.lang.ArithmeticException: Divide by zero
java.lang.ArithmeticException: Divide by zero
        at clojure.lang.Numbers.divide(Numbers.java:142)
        at user.eval__2138.invoke(Unknown Source)
        at clojure.lang.Compiler.eval(Compiler.java:3847)
        at clojure.lang.Repl.main(Repl.java:75)
使用者定義異常
[編輯 | 編輯原始碼]

如前所述,Clojure 中的所有異常都必須是 java.lang.Exception(或通常來說是 java.lang.Throwable,它是 Exception 的超類)的子類。這意味著即使您想在 Clojure 中定義自己的異常,也需要從 Exception 派生它。

別擔心,這比聽起來容易得多:)

Clojure API 提供了一個名為 gen-and-load-class 的函式,該函式可用於擴充套件 java.lang.Exception 以用於使用者定義的異常。gen-and-load-class 會生成並立即載入指定類的位元組碼。

現在,與其說得太多,不如我們快速看一下程式碼。

(gen-and-load-class 'user.UserException :extends Exception)

(defn user-exception-test []
  (try
    (throw (new user.UserException "msg: user exception was here!!"))
    (catch user.UserException e
      (prn "caught exception" e))
    (finally (prn "finally clause invoked!!!"))))

在這裡,我們正在建立一個名為 'user.UserException 的新類,該類擴充套件了 java.lang.Exception。我們使用特殊形式 (new Classname-symbol args*) 建立 user.UserException 的例項。然後將其**丟擲**。

有時您可能會遇到類似 (user.UserException. "msg: user exception was here!!") 的程式碼。這只是另一種說 new 的方式。請注意 user.UserException 後的 .(點)。這做了完全相同的事情。

以下是互動

user=> (load-file "except.clj")
#'user/user-exception-test

user=> (user-exception-test)
"caught exception" user.UserException: msg: user exception was here!!
"finally clause invoked!!!"
nil
user=>

因此,在這裡,我們同時呼叫了 catchfinally 子句。就這樣。

藉助 Clojure 對 Java 互動的支援,使用者也可以在 Java 中建立異常,並在 Clojure 中捕獲它們,但通常在 Clojure 中建立異常更方便。

變異工具

[編輯 | 編輯原始碼]
員工記錄操作
[編輯 | 編輯原始碼]

Clojure 中的資料結構和序列是不可變的,如 Clojure_Programming/Concepts#Structures 中介紹的示例所示(建議讀者先閱讀該部分)。

雖然不可變資料有其優點,但任何規模合理的專案都將要求程式設計師維護某種狀態。在使用不可變序列和資料結構的語言中管理狀態是習慣於允許對資料進行變異的程式語言的人們經常感到困惑的地方。

Rich Hickey 撰寫了一篇關於 Clojure 方法的好文章 [http://clojure.org/state 值和變化 - Clojure 對身份和狀態的處理方法]。

觀看 Clojure 併發螢幕廣播 可能會有所幫助,因為本節中使用了一些這些概念。特別是**ref** 和**事務**。

在本節中,我們建立了一個簡單的員工記錄集,並提供了以下功能

  • 新增員工
  • 按姓名刪除員工
  • 按姓名更改員工角色

此示例有意保持簡單,因為目的是展示狀態和變異工具,而不是提供完整的功能。

讓我們深入研究程式碼。

(alias 'set 'clojure.set)   ; use set/fn-name rather than clojure.set/fn-name

(defstruct employee
           :name :id :role) ; == (def employee (create-struct :name :id ..))

(def employee-records (ref #{}))

;;;===================================
;;; Private Functions: No Side-effects
;;;===================================

(defn- update-role [n r recs]
  (let [rec    (set/select #(= (:name %) n) recs)
        others (set/select #(not (= (:name %) n)) recs)]
    (set/union (map #(set [(assoc % :role r)]) rec) others)))

(defn- delete-by-name [n recs]
  (set/select #(not (= (:name %) n)) recs))

;;;=============================================
;;; Public Function: Update Ref employee-records
;;;=============================================
(defn update-employee-role [n r]
  "update the role for employee named n to the new role r"
  (dosync 
    (ref-set employee-records (update-role n r @employee-records))))

(defn delete-employee-by-name [n]
  "delete employee with name n"
  (dosync
    (ref-set employee-records
             (delete-by-name n @employee-records))))

(defn add-employee [e]
  "add new employee e to employee-records"
  (dosync (commute employee-records conj e)))

;;;=========================
;;; initialize employee data
;;;=========================
(add-employee (struct employee "Jack" 0 :Engineer))
(add-employee (struct employee "Jill" 1 :Finance))
(add-employee (struct-map employee :name "Hill" :id 2 :role :Stand))

在開頭幾行中,我們定義了 employee 結構體。之後有趣的定義是 employee-records

(def employee-records (ref #{}))

在 Clojure 中,ref 允許使用事務來變異儲存位置。

user=> (def x (ref [1 2 3]))
#'user/x
user=> x
clojure.lang.Ref@128594c
user=> @x
[1 2 3]
user=> (deref x)
[1 2 3]
user=>

接下來,我們使用 defn- 定義了私有函式 update-roledelete-by-name(請注意末尾的減號“-”)。請注意,這些是 純函式,沒有任何副作用。

update-role 接受員工姓名 n、新角色 r 和員工記錄表 recs。由於序列是不可變的,因此此函式返回一個**新的**記錄表,其中員工角色已相應更新。delete-by-name 也以類似的方式工作,在刪除相關員工記錄後返回一個新的員工表。

有關 set API 的說明,請參閱 Clojure API 參考

我們還沒有檢視如何維護狀態。這是透過清單中的公共函式 update-employee-roledelete-employee-by-nameadd-employee 來完成的。

這些函式將記錄處理的工作委託給私有函式。需要注意的重要事項是使用了以下函式

  • ref-set 設定 ref 的值。
  • dosync 是必需的,因為 ref 只能在事務中更新,而 dosync 會設定事務。
  • commute 更新 ref 的事務內值。

有關這些函式的詳細說明,請參閱 API 參考中的 ref 部分

add-employee 函式非常簡單,因此沒有將其分解為私有函式和公共函式。

原始碼清單在最後用示例資料初始化記錄。

以下是該程式的互動方式。

user=> (load-file "employee.clj")
#{{:name "Jack", :id 0, :role :Engineer} {:name "Hill", :id 2, :role :Stand} {:name "Jill", :id 1, :role :Finance}}

user=> @employee-records
#{{:name "Jack", :id 0, :role :Engineer} {:name "Hill", :id 2, :role :Stand} {:name "Jill", :id 1, :role :Finance}}

user=> (add-employee (struct employee "James" 3 :Bond))
#{{:name "James", :id 3, :role :Bond} {:name "Jack", :id 0, :role :Engineer} {:name "Hill", :id 2, :role :Stand} {:name "Jill", :id 1, :role :Finance}}
user=> @employee-records
#{{:name "James", :id 3, :role :Bond} {:name "Jack", :id 0, :role :Engineer} {:name "Hill", :id 2, :role :Stand} {:name "Jill", :id 1, :role :Finance}}

user=> (update-employee-role "Jill" :Sr.Finance)
#{{:name "James", :id 3, :role :Bond} {:name "Jack", :id 0, :role :Engineer} {:name "Hill", :id 2, :role :Stand} {:name "Jill", :id 1, :role :Sr.Finance}}
user=> @employee-records
#{{:name "James", :id 3, :role :Bond} {:name "Jack", :id 0, :role :Engineer} {:name "Hill", :id 2, :role :Stand} {:name "Jill", :id 1, :role :Sr.Finance}}

user=> (delete-employee-by-name "Hill")
#{{:name "James", :id 3, :role :Bond} {:name "Jack", :id 0, :role :Engineer} {:name "Jill", :id 1, :role :Sr.Finance}}
user=> @employee-records
#{{:name "James", :id 3, :role :Bond} {:name "Jack", :id 0, :role :Engineer} {:name "Jill", :id 1, :role :Sr.Finance}}

關於該程式需要注意兩點

  • 使用引用和事務使該程式天生執行緒安全。如果我們想要擴充套件該程式以用於多執行緒環境(使用 Clojure 代理),它將在進行最小更改的情況下進行擴充套件。
  • 將純功能與管理狀態的公共函式分開,更容易確保功能的正確性,因為純函式更容易測試。

名稱空間 [1]

[編輯 | 編輯原始碼]
  • 使用require載入Clojure庫
  • 使用refer引用當前名稱空間中的函式
  • 使用use在一個步驟中載入並引用所有內容
  • 使用import引用當前名稱空間中的Java類

Require

1. 您可以使用(require libname)載入任何Clojure庫的程式碼。嘗試使用clojure.contrib.math

     (require clojure.contrib.math)

2. 然後列印名稱空間中可用名稱的目錄

     (dir clojure.contrib.math)

3. 展示使用lcm計算最小公倍數

     1	(clojure.contrib.math/lcm 11 41)
     2	-> 451

4. 在每個函式呼叫中寫出名稱空間字首很麻煩,因此您可以使用as指定一個更短的別名

     (require [clojure.contrib.math :as m])

5. 呼叫更短的格式要容易得多

     1	(m/lcm 120 1000)
     2	-> 3000

6. 您可以使用以下命令檢視所有載入的名稱空間

     (all-ns)
Refer和Use
[編輯 | 編輯原始碼]

1. 使用沒有名稱空間字首的函式會更容易。您可以透過引用名稱來做到這一點,這會在當前名稱空間中建立一個對該名稱的引用

     (refer 'clojure.contrib.math)

2. 現在您可以直接呼叫lcm

     1	(lcm 16 30)
     2	-> 240

3. 如果您想在一步驟中載入並引用所有內容,請呼叫use

     (use 'clojure.contrib.math)

4. 引用庫會引用其所有名稱。這通常不可取,因為

  • 它沒有清楚地記錄對閱讀者的意圖
  • 它引入了比您需要的更多名稱,這會導致命名衝突

相反,請使用以下風格僅指定您想要的那些名稱

     (use '[clojure.contrib.math :only (lcm)])

:only選項在所有名稱空間管理表單中都可用。(還有一個:exclude選項,它按照您的預期工作。)

5. 變數*ns*始終包含當前名稱空間,您可以透過呼叫以下命令檢視當前名稱空間引用的名稱

     (ns-refers *ns*)

6. refers對映通常非常大。如果您只對一個符號感興趣,請將該符號傳遞給呼叫ns-refers的結果

     1	((ns-refers *ns*) 'dir)
     2	-> #'clojure.contrib.ns-utils/dir

1. 匯入類似於引用,但用於Java類而不是Clojure名稱空間。而不是

     (java.io.File. "woozle")

你可以說

     1	(import java.io.File)
     2	(File. "woozle")

2. 您可以使用以下表單匯入Java包中的多個類

     (import [package Class Class])

例如

     1	(import [java.util Date Random])
     2	(Date. (long (.nextInt (Random.))))

3. 對Lisp不熟悉的新手程式設計師通常會被像上面日期建立這樣的“內向外”讀取形式所困擾。從內部開始,您

  • 獲得一個新的Random
  • 獲取下一個隨機整數
  • 將其轉換為long
  • 將long傳遞給Date建構函式

您不必在Clojure中編寫內向外程式碼。->宏接受其第一個形式,並將其作為第一個引數傳遞給其下一個形式。然後,結果成為下一個形式的第一個引數,依此類推。它比描述更容易閱讀

     1	(-> (Random.) (.nextInt) (long) (Date.))
     2	-> #<Date Sun Dec 21 12:47:20 EST 1969>
Load和Reload
[編輯 | 編輯原始碼]

REPL並非適合所有情況。對於您計劃保留的工作,您需要將原始碼放在單獨的檔案中。以下是在建立自己的Clojure名稱空間時需要記住的經驗法則。

1. Clojure名稱空間(也稱為庫)等效於Java包。

2. Clojure遵循Java的目錄和檔案命名約定,但遵循Lisp的名稱空間名稱命名約定。因此,一個Clojure名稱空間com.my-app.utils將位於名為com/my_app/utils.clj的路徑中。特別注意下劃線/連字元之間的區別。

3. Clojure檔案通常以名稱空間宣告開頭,例如

     (ns com.my-app.utils)

4. 在上一節中介紹的import/use/refer/require語法用於REPL。名稱空間宣告允許類似的表單 - 足夠相似以幫助記憶,但也足夠不同以至於讓人困惑。在REPL中的以下表單

     1	(use 'foo.bar)
     2	(require 'baz.quux)
     3	(import '[java.util Date Random])

在原始碼檔案中看起來像這樣

     1	(ns
     2	 com.my-app.utils
     3	 (:use foo.bar)
     4	 (:require baz.quux)
     5	 (:import [java.util Date Random]))

符號變成關鍵字,不再需要引用。

5. 在撰寫本文時,使用名稱空間錯誤執行的錯誤訊息很模糊。小心。

現在讓我們嘗試建立一個原始碼檔案。我們現在不會費心進行顯式編譯。Clojure會自動(並且快速)編譯類路徑上的原始碼檔案。相反,我們只需將Clojure(.clj)檔案新增到src目錄中即可。

1. 在src目錄中建立一個名為student/dialect.clj的檔案,其中包含適當的名稱空間宣告

     (ns student.dialect)

2. 現在,實現一個簡單的canadianize函式,該函式接受一個字串,並在其末尾新增,eh?

     (defn canadianize [sentence] (str sentence ", eh"))

3. 從您的REPL中,使用新的名稱空間

     (use 'student.dialect)

4. 現在試一試。

     1	(canadianize "Hello, world.")
     2	-> "Hello, world., eh"

5. 糟糕!我們需要從輸入的末尾刪除句號。幸運的是,clojure.contrib.str-utils2提供了chop。返回student/dialect.clj並新增clojure.contrib.str-utils2中的require

     (ns student.dialect (:require [clojure.contrib.str-utils2 :as s]))

6. 現在,更新canadianize以使用chop

     (defn canadianize [sentence] (str (s/chop sentence) ", eh?"))

7. 如果您只是嘗試從REPL中呼叫canadianize,您將看不到新的更改,因為程式碼已經載入了。但是,您可以使用帶有reload(或reload-all)的名稱空間表單來重新載入名稱空間(及其依賴項)。

     (use :reload 'student.dialect)

8. 現在您應該看到canadianize的新版本

     1	(canadianize "Hello, world.")
     2	-> "Hello, world, eh?"

函數語言程式設計

[編輯 | 編輯原始碼]

匿名函式

[編輯 | 編輯原始碼]

Clojure使用fn或更短的reader宏 #(..)支援匿名函式#(..)很方便,因為它簡潔,但有些限制,因為#(..)形式不能巢狀

以下是一些使用兩種形式的示例

user=> ((fn [x] (* x x)) 3)
9

user=> (map #(list %1 (inc %2)) [1 2 3] [1 2 3])
((1 2) (2 3) (3 4))

user=> (map (fn [x y] (list x (inc y))) [1 2 3] [1 2 3])
((1 2) (2 3) (3 4))

user=> (map #(list % (inc %)) [1 2 3])
((1 2) (2 3) (3 4))

user=> (map (fn [x] (list x (inc x))) [1 2 3])
((1 2) (2 3) (3 4))

user=> (#(apply str %&) "Hello")
"Hello"

user=> (#(apply str %&) "Hello" ", " "World!")
"Hello, World!"

請注意,在#(..)形式中,%N用於引數(從1開始),%&用於剩餘引數。%%1的同義詞。

序列的惰性求值

[編輯 | 編輯原始碼]

本節嘗試逐步瀏覽一些程式碼,以更好地瞭解Clojure對序列的惰性求值以及它可能如何有用。我們還測量記憶體和時間以更好地瞭解發生了什麼。

假設我們想要對包含十億個專案的列表中的記錄進行big-computation(每次1秒)。通常我們可能不需要處理所有十億個專案(例如,我們可能只需要過濾後的子集)。

讓我們定義一個名為free-mem的小型實用程式函式來幫助我們監控記憶體使用情況,以及另一個名為big-computation的函式,該函式需要1秒鐘才能完成其工作。

(defn free-mem [] (.freeMemory (Runtime/getRuntime)))

(defn big-computation [x] (Thread/sleep 1000) (* 10 x))

在上面的函式中,我們使用java.lang.Runtimejava.lang.Thread來獲取可用記憶體並支援休眠。

我們還將使用內建函式time來測量我們的效能。

以下是REPL中的簡單用法

user=> (defn free-mem [] (.freeMemory (Runtime/getRuntime)))
#'user/free-mem

user=> (defn big-computation [x] (Thread/sleep 1000) (* 10 x))
#'user/big-computation

user=> (time (big-computation 1))
"Elapsed time: 1000.339953 msecs"
10

現在我們定義一個包含十億個數字的列表,名為nums

user=> (time (def nums (range 1000000000)))
"Elapsed time: 0.166994 msecs"
#'user/nums

請注意,Clojure建立包含十億個數字的列表需要0.17毫秒。這是因為列表實際上並沒有建立。使用者只是從Clojure那裡得到一個承諾,即在需要時將返回來自該列表的適當數字。

現在,假設我們想要對來自該列表的10000到10005的x應用big-computation

這是它的程式碼

;; The comments below should be read in the numbered order
;; to better understand this code.

(time                              ; [7] time the transaction
  (def v                           ; [6] save vector as v
    (apply vector                  ; [5] turn the list into a vector
           (map big-computation    ; [4] process each item for 1 second
                (take 5            ; [3] take first 5 from filtered items
                      (filter      ; [2] filter items 10000 to 10010
                        (fn [x] (and (> x 10000) (< x 10010)))
                        nums)))))) ; [1] nums = 1 billion items

將此程式碼放在REPL中,我們將得到以下結果

user=> (free-mem)
2598000
user=> (time (def v (apply vector (map big-computation (take 5 (filter (fn [x] (and (> x 10000) (< x 10010))) nums))))))
"Elapsed time: 5036.234311 msecs"
#'user/v
user=> (free-mem)
2728800

程式碼塊中的註釋指示了這段程式碼的工作原理。它花了我們大約5秒鐘來執行這段程式碼。以下是一些需要注意的地方

  • 從列表中過濾出編號為10000到10010的專案沒有花費我們10000秒
  • 從10個過濾後的列表中獲取前5個專案沒有花費我們10秒
  • 總的來說,計算只花了大約5秒鐘,這基本上就是計算時間。
  • 即使我們現在有處理十億個記錄的承諾,可用記憶體數量實際上也相同。(它實際上似乎有所增加,因為發生了垃圾回收)


現在,如果我們訪問v,它只需要忽略不計的時間。

user=> (time (seq v))
"Elapsed time: 0.042045 msecs"
(100010 100020 100030 100040 100050)
user=>

需要注意的另一點是,惰性序列並不意味著每次都會進行計算;一旦計算完成,它就會被快取。

嘗試以下操作

user=> (time (def comps (map big-computation nums)))
"Elapsed time: 0.113564 msecs"
#'user/comps

user=> (defn t5 [] (take 5 comps))
#'user/t5

user=> (time (doall (t5)))
"Elapsed time: 5010.49418 msecs"
(0 10 20 30 40)

user=> (time (doall (t5)))
"Elapsed time: 0.096104 msecs"
(0 10 20 30 40)

user=>

在第一步中,我們將big-computation對映到十億個nums。然後,我們定義一個名為t5的函式,該函式從comps中獲取5個計算。觀察到,t5第一次需要5秒,之後它只需要忽略不計的時間。這是因為一旦計算完成,結果就會被快取以備將來使用。由於t5的結果也是惰性的,因此需要doall來強制它在time返回REPL之前被急切地求值。

惰性資料結構可以提供顯著的優勢,前提是程式被設計為利用它。為惰性序列和無限資料結構設計程式是與在C和Java等語言中急切地執行計算相比的一種正規化轉變,而是在提供計算承諾

本節內容基於這封郵件中的 Clojure 論壇討論。

無限資料來源

[編輯 | 編輯原始碼]

由於 Clojure 支援惰性求值序列,因此可以在 Clojure 中使用無限資料來源。可以使用以下程式碼定義無限序列 (0 1 2 3 4 5 ....)(range)從 Clojure 1.2 版開始:[2]

 (def nums (range))         ; Old version (def nums (iterate inc 0))
 ;; ⇒ #'user/nums
 (take 5 nums)
 ;; ⇒ (0 1 2 3 4)
 (drop 5 (take 11 nums))
 ;; ⇒ (5 6 7 8 9 10)

這裡我們看到了兩個用於建立從 0 開始的無限數字列表的函式。由於 Clojure 支援惰性序列,因此只會生成所需的專案並從該列表的頭部取出。在上述情況下,如果您在提示符處直接輸入(range)(iterate inc 0)直接在提示符處,[http://clojure.org/reader讀取器] 將會不斷獲取下一個數字,您需要終止程序。

(iterate f x) 是一個函式,它不斷將 f 應用於之前將 f 應用於 x 的結果。這意味著,結果是 ...(f(f(f(f .....(f(f(f x)))))...(iterate inc 0) 首先返回 0 作為結果,然後 (inc 0) => 1,然後 (inc (inc 0)) => 2,依此類推。

(take n coll) 基本上會從集合中刪除 n 個專案。此主題有很多變體

  • (take n coll)
  • (take-nth n coll)
  • (take-last n coll)
  • (take-while pred coll)
  • (drop n coll)
  • (drop-while pred coll)

建議讀者檢視Clojure 序列 API以瞭解更多詳細資訊。

列表推導

[編輯 | 編輯原始碼]

列表推導是語言提供的構造,使從舊列表中輕鬆建立新列表成為可能。聽起來很簡單,但它是一個非常強大的概念。Clojure 對列表推導提供了良好的支援。

假設我們想要一個包含 x + 1 的集合,其中 x 可被 4 整除,x0 開始。

以下是使用 Clojure 完成此操作的一種方法

(def nums (iterate inc 0))
;; ⇒ #'user/nums
(def s (for [x nums :when (zero? (rem x 4))] (inc x)))
;; ⇒ #'user/s
(take 5 s)
;; ⇒ (1 5 9 13 17)

nums 是我們在上一節中看到的無限數字列表。我們需要 (def s ...) 用於集合,因為我們正在建立一個無限的數字源。在提示符處直接執行它會導致 讀取器 不斷從該源中提取數字。

這裡的關鍵構造是 for 宏。這裡表示式 [x nums ... 表示 x 會從 nums 中逐個提取出來。下一條子句 .. :when (zero? (rem x 4)) .. 基本上表示只有當 x 滿足此條件時才會將其提取出來。一旦提取了 x,inc 就會應用於它。將所有這些繫結到 s 會得到一個無限集合。因此,我們看到了 (take 5 s) 和預期的結果。

另一種實現相同結果的方法是使用 mapfilter

(def s (map inc (filter (fn [x] (zero? (rem x 4))) nums)))
;; ⇒ #'user/s
(take 5 s)
;; ⇒ (1 5 9 13 17)

這裡我們建立了一個謂詞 (fn [x] (zero? (rem x 4))),並且只有當此謂詞滿足時才會從 nums 中提取 x。這是透過 filter 完成的。請注意,由於 Clojure 是惰性的,因此 filter 給出的只是一個提供滿足謂詞的下一個數字的承諾。它不會(在這種情況下也不能)評估整個列表。一旦我們有了這個 x 流,只需將 inc 對映到它 (map inc ... 即可。

列表推導(即 for)和 map/filter 之間的選擇很大程度上取決於使用者偏好。兩者之間沒有主要優勢。

序列函式

[編輯 | 編輯原始碼]
(first coll)
[編輯 | 編輯原始碼]

獲取序列的第一個元素。對於空序列或 nil 返回 nil

 (first (list 1 2 3 4))
 ;; ⇒ 1
 (first (list))
 ;; ⇒ nil
 (first nil)
 ;; ⇒ nil
 (map first [[1 2 3] "Test" (list 'hi 'bye)])
 ;; ⇒ (1 \T hi)
 (first (drop 3 (list 1 2 3 4)))
 ;; ⇒ 4
(rest coll)
[編輯 | 編輯原始碼]

獲取序列中除了第一個元素之外的所有元素。對於空序列或 nil 返回 nil

 (rest (list 1 2 3 4))
 ;; ⇒ (2 3 4)
 (rest (list))
 ;; ⇒ nil
 (rest nil)
 ;; ⇒ nil
 (map rest [[1 2 3] "Test" (list 'hi 'bye)])
 ;; ⇒ ((2 3) (\e \s \t) (bye))
 (rest (take 3 (list 1 2 3 4)))
 ;; ⇒ (2 3)
(map f colls*)
[編輯 | 編輯原始碼]

f 惰性應用於序列中的每個專案,返回一個包含 f 返回值的惰性序列。

由於提供的函式始終返回 true,因此這兩個函式都返回一個包含 true 的序列,重複十次。

 (map (fn [x] true) (range 10))
 ;; ⇒ (true true true true true true true true true true)
 (map (constantly true) (range 10)) 
 ;; ⇒ (true true true true true true true true true true)

這兩個函式都將其引數乘以 2,因此 (map ...) 返回一個序列,其中原始序列中的每個專案都乘以 2。

 (map (fn [x] (* 2 x)) (range 10))
 ;; ⇒ (0 2 4 6 8 10 12 14 16 18)
 (map (partial * 2) (range 10))
 ;; ⇒ (0 2 4 6 8 10 12 14 16 18)

(map ...) 可以接受您提供的任意多個序列(儘管它至少需要一個序列),但函式引數必須接受與序列數量相同的引數。

因此,這兩個函式給出了乘在一起的序列

 (map (fn [a b] (* a b)) (range 10) (range 10))
 ;; ⇒ (0 1 4 9 16 25 36 49 64 81)
 (map * (range 10) (range 10))
 ;; ⇒ (0 1 4 9 16 25 36 49 64 81)

但第一個函式只接受兩個序列作為引數,而第二個函式接受任意多個序列作為引數。

 (map (fn [a b] (* a b)) (range 10) (range 10) (range 10))
 ;; ⇒ java.lang.IllegalArgumentException: Wrong number of args passed
 (map * (range 10) (range 10) (range 10))
 ;; ⇒ (0 1 8 27 64 125 216 343 512 729)

(map ...) 將在到達任何提供的序列的末尾時停止求值,因此在這三個示例中,(map ...) 在 5 個專案處停止求值(最短序列的長度),儘管第二和第三個示例提供的序列長度大於 5 個專案(在第三個示例中,較長的序列是無限長度的)。

它們中的每一個都接受一個僅包含數字 2 的序列和一個包含數字 (0 1 2 3 4) 的序列,並將它們乘在一起。

 (map * (replicate 5 2) (range 5))
 ;; ⇒ (0 2 4 6 8)
 (map * (replicate 10 2) (range 5))
 ;; ⇒ (0 2 4 6 8)
 (map * (repeat 2) (range 5))
 ;; ⇒ (0 2 4 6 8)
(every? pred coll)
[編輯 | 編輯原始碼]

如果 pred 對於序列中的每個專案都為 true,則返回 true。否則返回 false。pred 在這裡是一個接受單個引數並返回 true 或 false 的函式。

由於此函式始終返回 true,因此 (every? ...) 的值為 true。請注意,這兩個函式表達了相同的內容。

 (every? (fn [x] true) (range 10))
 ;; ⇒ true
 (every? (constantly true) (range 10))
 ;; ⇒ true

(pos? x) 當其引數大於零時返回 true。由於 (range 10) 給出一個從 0 到 9 的數字序列,而 (range 1 10) 給出一個從 1 到 10 的數字序列,因此 (pos? x) 對於第一個序列返回 false 一次,而對於第二個序列永遠不會返回 false。

 (every? pos? (range 10))
 ;; ⇒ false
 (every? pos? (range 1 10))
 ;; ⇒ true

此函式當其引數為偶數時返回 true。由於 1 到 10 之間的範圍和序列 (1 3 5 7 9) 包含奇數,因此 (every? ...) 返回 false。

由於序列 (2 4 6 8 10) 僅包含偶數,因此 (every? ...) 返回 true。

 (every? (fn [x] (= 0 (rem x 2))) (range 1 10))
 ;; ⇒ false
 (every? (fn [x] (= 0 (rem x 2))) (range 1 10 2))
 ;; ⇒ false
 (every? (fn [x] (= 0 (rem x 2))) (range 2 10 2))
 ;; ⇒ true

如果我需要在其他地方檢查一個數字是否為偶數,我可能會這樣寫,在將 (even? num) 作為引數傳遞給 (every? ...) 之前,先將其定義為一個實際的函式

 (defn even? [num] (= 0 (rem num 2)))
 ;; ⇒ #<Var: user/even?>
 (every? even? (range 1 10 2))
 ;; ⇒ false
 (every? even? (range 2 10 2))
 ;; ⇒ true


補充函式: (not-every? pred coll)

返回 (every? pred coll) 的補充值。如果 pred 對於序列中的所有專案都為 true,則返回 false;否則返回 true。

 (not-every? pos? (range 10))
 ;; ⇒ true
 (not-every? pos? (range 1 10))
 ;; ⇒ false

迴圈和迭代

[編輯 | 編輯原始碼]

三種不同的方式迴圈從 1 到 20,步長為 2,每次列印迴圈索引(來自郵件列表討論

 ;; Version 1
 (loop [i 1]
   (when (< i 20)
     (println i)
     (recur (+ 2 i))))
 
 ;; Version 2
 (dorun (for [i (range 1 20 2)]
          (println i)))
 
 ;; Version 3
 (doseq [i (range 1 20 2)]
   (println i))

相互遞迴

[編輯 | 編輯原始碼]

相互遞迴在 Clojure 中很棘手,但卻是可行的。(defn ...) 的形式只允許函式體引用自身或以前存在的名稱。但是,Clojure 允許以以下方式動態重新定義函式繫結

 ;;; Mutual recursion example
 
 ;; Forward declaration
 (def even?)
 
 ;; Define odd in terms of 0 or even
 (defn odd? [n]
   (if (zero? n)
       false
       (even? (dec n))))
 
 ;; Define even? in terms of 0 or odd
 (defn even? [n]
   (if (zero? n)
       true
       (odd? (dec n))))
 
 ;; Is 3 even or odd?
 (even? 3) 
 ;; ⇒ false

在使用 let 定義的內部函式中,相互遞迴是不可能的。要宣告一組私有遞迴函式,可以使用上述技術,將 defn 替換為 defn-,這將生成私有定義。

但是,可以使用 looprecur 模擬相互遞迴函式。

(use 'clojure.contrib.fcase)

(defmacro multi-loop
  [vars & clauses]
  (let [loop-var  (gensym "multiloop__")
        kickstart (first clauses)
        loop-vars (into [loop-var kickstart] vars)]
    `(loop ~loop-vars
       (case ~loop-var
          ~@clauses))))

(defn even?
  [n]
  (multi-loop [n n]
    :even (if (zero? n)
            true
            (recur :odd (dec n)))
    :odd  (if (zero? n)
            false
            (recur :even (dec n)))))

集合抽象

[編輯 | 編輯原始碼]

http://blog.n01se.net/?p=33 上可以找到有關如何編寫宏的詳細介紹,作者是 Chouser。

宏用於在編譯時轉換資料結構。讓我們開發一個新的 `do1` 宏。Clojure 的 `do` 特殊形式會評估所有包含的表示式以獲取副作用,並返回最後一個表示式的返回值。`do1` 的行為類似,但返回第一個子表示式的值。

一開始應該先考慮宏應該如何呼叫。

(do1
  :x
  :y
  :z)

返回值應該是 `:x`。接下來要考慮的是如何手動實現這個功能。

(let [x :x]
  :y
  :z
  x)

首先評估 :x,然後是 :y 和 :z。最後,let 評估到 :x 的評估結果。這可以透過使用 `defmacro` 和 `` 來轉換為宏。

(defmacro do1
  [fform & rforms]
  `(let [x# ~fform]
     ~@rforms
     x#))

所以這裡發生了什麼。這只是一個簡單的翻譯。我們使用 `let` 為第一個表示式的結果建立一個臨時位置。由於我們不能簡單地使用某個名稱(它可能在使用者程式碼中被使用),因此我們使用 `x#` 生成一個新的名稱。# 是 Clojure 的特殊符號,它幫助我們:它生成一個新的名稱,保證不會被使用者程式碼使用。`~` 對第一個表示式進行“解引用”,即 `~fform` 被第一個引數替換。然後使用 `~@` 插入剩餘的表示式。使用 `@` 基本上從以下表達式中刪除了一組 ()。最後,我們再次透過 `x#` 引用第一個表示式的結果。

我們可以使用 `(macroexpand-1 '(do1 :x :y :z))` 檢查宏的展開。

來自 `clojure.contrib` 的 lib 包現在已整合到 clojure 中。定義可以由其他指令碼載入的庫很容易。假設我們有一個很棒的 `add1` 函式,我們想提供給其他開發者。那麼我們需要什麼?首先,我們確定一個名稱空間,例如 `example.ourlib`。現在,我們必須在類路徑中建立一個檔名“example/ourlib.clj”的檔案。內容非常簡單。

(ns example.ourlib)

(defn add1
  [x]
  (add x 1))

現在我們只需要使用 `ns` 的功能。假設我們有另一個檔案,我們想使用我們的函式。`ns` 允許我們透過多種方式指定我們的需求。最簡單的是 `:require`

(ns example.otherns
  (:require example.ourlib))

(defn check-size
  [x]
  (if (too-small x)
    (example.ourlib/add1 x)
    x))

但是如果我們多次需要 `add1` 函式呢?我們必須在前面始終鍵入名稱空間。我們可以新增一個 `(refer 'example.ourlib)`,但是我們可以更容易地做到這一點。只需使用 `:use` 而不是 `:require`!`:use` 載入庫,就像 `:require` 一樣,並立即引用名稱空間。

因此,我們現在已經有兩個小型庫,它們可能在第三個程式中使用。

(ns example.thirdns
  (:require example.ourlib)
  (:require example.otherns))

同樣,我們也可以節省一些輸入。類似於 `import`,我們可以提取庫名稱空間的通用字首。

(ns example.thirdns
  (:require (example ourlib otherns)))

當然,`ourlib` 包含 738 個以上的函式,而不僅僅是上面顯示的那些。我們並不真正想使用 `use`,因為引入如此多的名稱會導致衝突,但我們也不想一直鍵入名稱空間。所以我們首先使用 `alias`。但是等等!你猜對了:`ns` 又幫助我們了。

(ns example.otherns
  (:require (example [ourlib :as ol])))

`:as` 負責別名處理,現在我們可以將 `add1` 函式稱為 `ol/add1`!

到目前為止,它已經相當不錯了。但是,如果我們稍微考慮一下我們的原始碼組織,我們可能會發現,在一個檔案中放 739 個函式可能不是最好的主意。因此,我們決定進行一些重構。我們建立一個檔案“example/ourlib/add1.clj”並將我們的函式放在那裡。我們不希望使用者必須載入多個檔案而不是一個檔案,因此我們修改“example/ourlib.clj”檔案以如下方式載入任何其他檔案。

(ns example.ourlib
  (:load "ourlib/add1"
         "ourlib/otherfunc"
         "ourlib/morefuncs"))

因此,使用者仍然載入“公共”example.ourlib 庫,它負責載入其餘部分。(`:load` 實現包含程式碼,為要載入的檔案提供“.clj”字尾)

有關更多資訊,請參閱 `require` 的文件字串 - `(doc require)`。

參考文獻

[編輯 | 編輯原始碼]
華夏公益教科書