Unknown perls from the Clojure standard library 筆記

看完 ClojuTRE 2015Unknown pearls from the Clojure standard library - Renzo Borgatti 演講後,來紀錄個筆記。

這場演講介紹了一些在 clojure.core 裡面的函式,這些函式平常可能不太有機會用到,但是可以協助我們除錯程式的問題。

投影片: 下載連結

destructure

destructure (解構) 在 Clojure 裡面是個非常實用的功能,可以方便我們對資料直接指派變數去代替它,如下:

No destructuring

(let [data [1 2 3]
      a (nth data 0)
      b (nth data 1)
      c (nth data 2)]
  (println "a:" a "b:" b "c:" c))
;; => a: 1 b: 2 c: 3

With destructuring

 (let [[a b c] [1 2 3]]
   (println "a:" a "b:" b "c:" c))
 ;; => a: 1 b: 2 c: 3
 
 
 

我們可以透過 destructure 去觀察一個東西是如何被解構的,這邊是投影片給的範例:

(destructure '[[x y & others] v])

;; => [v2 v
;;     x (nth v2 0 nil)
;;     y (nth v2 1 nil)
;;     others (nthnext v2 2)]

當然投影片給的是整理後的結果,實際上我執行得到的結果是這樣的

[vec__23596
 v
 seq__23597
 (clojure.core/seq vec__23596)
 first__23598
 (clojure.core/first seq__23597)
 seq__23597
 (clojure.core/next seq__23597)
 x
 first__23598
 first__23598
 (clojure.core/first seq__23597)
 seq__23597
 (clojure.core/next seq__23597)
 y
 first__23598
 others
 seq__23597]

reductions

我們在 Clojure 中很常用 reduce 去將一個函數作用到 list 上的每兩個元素上,然後返回最後的結果,最常見的簡單函數就是一個 list 的元素全部相加

(reduce + (range 10))
;; => 45

reduce 的運作過程,則可以透過 reductions 來協助我們進行查看,可以看到這邊最後得到的 45 就是我們想要的結果。

(reductions + (range 10))
;; =>  (0 1 3 6 10 15 21 28 36 45)

test

在 Clojure 中,我們可以在 metadata 中設定好對一個函數的測試方式,然後呼叫 test 對該函數進行測試,這項功能很適合用在小函式的一些 assertion 測試上。

(defn add+
  {:test #(do
            (assert (= (add+ 2 3) 5))
            (assert (= (add+ 4 4) 8)))}
  [x y] (+ x y))

(test #'add+) ;; <= trigger
;; => :ok

你也可以透過 meta 去查看你這個函式的 metadata 或是測試用的函式資訊

(meta #'addd+)

;; =>
{:arglists ([x y]),
 :test #function[hello.core/fn--23678],
 :line 350,
 :column 4,
 :file "/home/coldnew/Workspace/hello/src/hello/cpre.clj",
 :name add+,
 :ns #namespace[hello.core]}

clojure.pprint/cl-format

clojure.pprint/cl-format 是 Clojure 移植 Common Lisp 的 format 函式,對於同時寫 Clojure 和 ClojureScript 的開發者而言, cl-format 可以同時用於 Clojure 和 ClojureScript 上,方便了不少。

(註: CLJS-324 ISSUE 尚未被解決前,Clojure 的 format 是無法用於 ClojureScript 上的)

如果要更多關於 cl-format 的使用,可以看看 Praticle Common Lisp 一書,我在 clojure/clojurescript 與 left-pad 一文亦有提到如何透過 cl-format 實作 Clojure/ClojureScript 皆可以用的 leftpad 函式。

投影片上給的範例則是這樣:

(clojure.pprint/cl-format nil "~:r" 1234)
;; => one thousand, two hundred thirty-fourth

(clojure.pprint/cl-format nil "~@r" 1234)
;; => MCCXXXIV

clojure.java.browse/browse-url

clojure.java.browse/browse-url 會呼叫系統預設的瀏覽器,開啟你所指定的網頁。

(clojure.java.browse/browse-url "http://localhost:3000")

clojure.java.javadoc/javadoc

Clojure 畢竟是 JVM 上的語言,有時候我們需要查看一些 javadoc,或是查看 Clojure 內部的 Java 實現,可以透過 clojure.java.javadoc/javadoc 來查看

(clojure.java.javadoc/javadoc (list* 1 []))
;; => open clojure.lang.Cons Javadoc

clojure.reflect/reflect

老實說我看了還是不知道這是什麼,也許是和 Java 的 reflection 有關,不過我們還是可以在 clojure.reflect/reflect 的文檔中看出一些東西

(require '[clojure.reflect :refer [reflect]])
(require '[clojure.pprint :refer [print-table]])

;; Here we have a simple function that prints the
;; important bits of the class definition in a table.
(->> String
     reflect
     :members
     print-table)

;; =>
;; |                    :name |           :return-type | :declaring-class |                                       :parameter-types |                       :exception-types |                        :flags |
;; |--------------------------+------------------------+------------------+--------------------------------------------------------+----------------------------------------+-------------------------------|
;; |               replaceAll |       java.lang.String | java.lang.String |                    [java.lang.String java.lang.String] |                                     [] |                    #{:public} |
;; |   CASE_INSENSITIVE_ORDER |                        | java.lang.String |                                                        |                                        |     #{:public :static :final} |
;; |                  indexOf |                    int | java.lang.String |                    [char<> int int char<> int int int] |                                     [] |                    #{:static} |
;; |           codePointCount |                    int | java.lang.String |                                              [int int] |                                     [] |                    #{:public} |
;; |                 getChars |                   void | java.lang.String |                                   [int int char<> int] |                                     [] |                    #{:public} |
;; |            regionMatches |                boolean | java.lang.String |                         [int java.lang.String int int] |                                     [] |                    #{:public} |
;; |                  isEmpty |                boolean | java.lang.String |                                                     [] |                                     [] |                    #{:public} |
;; |              codePointAt |                    int | java.lang.String |                                                  [int] |                                     [] |                    #{:public} |
;; |              lastIndexOf |                    int | java.lang.String |                                     [java.lang.String] |                                     [] |                    #{:public} |
;; |               startsWith |                boolean | java.lang.String |                                 [java.lang.String int] |                                     [] |                    #{:public} |
;; ...etc

講者在投影片中給的範例則是這個: (注意到結果是節錄呦~)

(require '[clojure.reflect :refer [reflect]])
(println (with-out-str (clojure.pprint/write (reflect :a))))

;; extract from a typical output:
{:name invoke,
 :return-type java.lang.Object,
 :declaring-class clojure.lang.Keyword,
 :parameter-types [java.lang.Object java.lang.Object],
 :exception-types [],
 :flags #{:public :final}}

clojure.inspector/inspect-tree

我們在處理一些樹狀資料時(ex: JSON 格式),有個圖示化的工具可以方便瀏覽所有資訊,這邊可以透過 clojure.inspector/inspect-tree 來查看

(require '[clojure.inspector :as i])

(def m {:a "a"
        :b {:c "c"
            :d [1 2 3]
            :e {:f "f"
                :g "g"
                :h "h"}}
        :i [1 2 3]
        :l {:m "m"
            :n "n"}
        :o [{:p "p" :q "q"}]})

(i/inspect-tree m)

這樣呼叫,會得到如下的視窗

clojure.lang.PersistentQueue

clojure.lang.persistentQueue 是 Clojure 下並未寫在 doc 上面的 Queue 實現 (java) ,我們可以用它來實作我們需要的 queue (佇列) 功能

基本上,你有以下幾種方法可以對你的 PersistentQueue 進行處理

  • peek

    取得 queue 最頂端的資料(head)

  • pop

    回傳不包含最頂端資料(head)的一個新的 PersistentQueue

  • conj

    將資料加入到 queue 的尾巴

  • empty?

    檢測 queue 是否為空的

  • seq

    將 queue 的資料變成序列 (sequence)

投影片上,講者是舉這範例:

(def e (clojure.lang.PersistentQueue/EMPTY))
(def buf (reduce conj e (range 10)))

(peek buf)
;; => 0

(peek (pop buf))
;; => 1

(peek (pop (pop buf)))
;; => 2

StackOverflow 則是有人舉出了如何透過 clojure.lang.persistentQueue 實現自己的 queue 函式

(defn queue
  ([] clojure.lang.PersistentQueue/EMPTY)
  ([coll] (reduce conj clojure.lang.PersistentQueue/EMPTY coll)))

(defmethod print-method clojure.lang.PersistentQueue
  [q ^java.io.Writer w]
  (.write w "#queue ")
  (print-method (sequence q) w))

(comment
  (let [*data-readers* {'queue #'queue}]
    (read-string (pr-str (queue [1 2 3])))))

fnil

我們在用 Clojure 處理東西的時候,有時候可能是資料本身就是 nil 的情況,這種狀況下對 nil 進行處理可能就會導致 Exception 的狀況發生

在傳統的 LISP 中,常以 or 作為一個保護,一旦遇到 nil 的情況,則回傳預設值

(or nil 10)
;; => 10

fnil 則是用來替你的函式多加一層保護,讓函式遇到 nil 的狀況可以避免一些 Exception 的發生

講者在投影片上提供的範例是這樣的:

(def m {:host "127.0.0.1" :port nil})
(update m :port (fnil #(Integer/parseInt %) "80"))
;; => {:host "127.0.0.1", :port 80}

(def m {:host "127.0.0.1" :port "8008"})
(update m :port (fnil #(Integer/parseInt %) "80"))

;; => {:host "127.0.0.1", :port 8008}

不過我們也可以將其拆開來看,這樣會更好理解

(Integer/parseInt "10")
;; => 10

(Integer/parseInt nil)
;; =>  Unhandled java.lang.NumberFormatException

((fnil #(Integer/parseInt %) 80) "1000")
;; => 1000

counted?

counted? 是 O(1) 的操作,用來查看目標是否具有實作 count 函式

(counted? [:a :b :c])
;; => true

(counted? '(:a :b :c))
;; => true

(counted? {:a 1 :b 2 :c 3})
;; => true

(counted? #{:a :b :c})
;; => true

(counted? "asdf")
;; => false

(counted? (into-array Integer/TYPE [1 2 3]))
;; => false

reversible?

reversible? 也是 O(1) 的操作,用來查看目標是否有實作 reversible 函式

(reversible? [])
;; =>  true

(reversible? (sorted-map))
;; =>  true

(reversible? (sorted-set))
;; =>  true

(reversible? '())
;; =>  false

(reversible? {})
;; =>  false

(reversible? #{})
;; =>  false

vector-of

vector-of 會根據你提供的類型 (permitive type) 來將參數轉換成相對應型別的向量 (vector)

可以使用的類型有 :int , :long , :float , :double , :byte , :short , :char , :boolean

(conj (vector-of :int) 1 2 3)
;; => [1 2 3]  ; <-- note, these are unboxed internally

(vector-of :int 1 2 3)
;; => [1 2 3]  ; same here

(vector-of :float 1 2 3)
;; => [1.0 2.0 3.0]

(type (conj (vector-of :int) 1 2 3))
;; => clojure.core.Vec

(type (conj (vector-of :float) 1 2 3))
;; => clojure.core.Vec

clojure.set/rename-keys

clojure.set/rename-keys 可以用來改變目前現有的 hash-map 的關鍵字 (keyword) 名稱

(require 'clojure.set)

(clojure.set/rename-keys {:a 1, :b 2} {:a :new-a, :b :new-b})
;; => {:new-a 1, :new-b 2}

(cloure.set/rename-keys {:a 1} {:b :new-b})
;; => {:a 1}

;; You need to be careful about key collisions.  You probably shouldn't
;; depend on the exact behavior.
(clojure.set/rename-keys {:a 1 :b 2} {:a :b})
;; => {:b 1}

(clojure.set/rename-keys  {:a 1 :b 2}  {:a :b :b :a})
;; => {:a 1}

clojure.data/diff

clojure.data/diff 用來對兩個序列(sequence)進行比較,並回傳比較的結果

(require 'clojure.data)

(clojure.data/diff {:a 1 :b 2} {:a 3 :b 2 :c 3})
;; => ({:a 1} {:a 3, :c 3} {:b 2})

(clojure.data/diff [1 2 3] [5 9 3 2 3 7])
;; => [[1 2] [5 9 nil 2 3 7] [nil nil 3]]

(clojure.data/diff (set [1 2 3]) (set [5 9 3 2 3 7]))
;; => [#{1}  #{7 9 5}        #{3 2}]

munge

munge 這個函式並未有文檔,因此只能實際看看用途了

(defn foo [] (println "foo"))
;; =>  #'user/foo

foo
;; => #<user$foo user$foo@a0dc71>

(munge foo)
;; => "user_DOLLARSIGN_foo_CIRCA_a0dc71"


(doseq [c (remove #(Character/isLetterOrDigit %) (map char (range 32 127)))]
  (println c "->" (munge c)))
;; Prints:
;;   ->
;; ! -> _BANG_
;; " -> _DOUBLEQUOTE_
;; # -> _SHARP_
;; $ -> $
;; % -> _PERCENT_
;; & -> _AMPERSAND_
;; ' -> _SINGLEQUOTE_
;; ( -> (
;; ) -> )
;; * -> _STAR_
;; + -> _PLUS_
;; , -> ,
;; - -> _
;; . -> .
;; / -> _SLASH_
;; : -> _COLON_
;; ; -> ;
;; < -> _LT_
;; = -> _EQ_
;; > -> _GT_
;; ? -> _QMARK_
;; @ -> _CIRCA_
;; [ -> _LBRACK_
;; \ -> _BSLASH_
;; ] -> _RBRACK_
;; ^ -> _CARET_
;; _ -> _
;; ` -> `
;; { -> _LBRACE_
;; | -> _BAR_
;; } -> _RBRACE_
;; ~ -> _TILDE_

gensym

gensym 會產生不衝突名稱的 symbol,這個應該是在 macro 實現時,Clojure 所使用到的函式

(gensym "foo")
;; => foo2020

(gensym "foo")
;; => foo2027

seque

seque 是一種 阻塞隊列(Linked Blocking Queue, LBQ) 的實作,這種實現是線程安全(thread safe)的,可以確保資料先進先出(first in first out, FIFO) 的狀況。

(let [start (System/nanoTime)
      q (seque
         (iterate
          #(do (Thread/sleep 400) (inc %))
          0))]
  (println "sleep five seconds...")
  (Thread/sleep 5000)
  (doseq [i (take 20 q)]
    (println (int (/ (- (System/nanoTime) start) 1e7))
             ":" i)))


;; The iterate form returns a lazy seq that delays nearly a half-second
;; before returning each subsequent item.  Here seque starts a thread
;; generating the lazy seq.

;; The body of the let allows the seque thread to get ahead by five seconds
;; before it begins consuming the seq using doseq.  The doseq prints a
;; timestamp and the value from the seq when it becomes available.  The
;; first 11 or so are available almost instantly, until the consuming
;; doseq catches up with the producing iterate, at which point the consumer
;; blocks for 400ms before each item can be printed.

;;sleep five seconds...
500 : 0
500 : 1
500 : 2
500 : 3
500 : 4
500 : 5
500 : 6
500 : 7
500 : 8
500 : 9
500 : 10
500 : 11
520 : 12
;; ......

clojure.zip/zippers

clojure.zip 是用來處理樹狀結構用函式庫,這篇文章有很好的解釋: (λx.Liu)Blog=Hacking: Understand clojure.zip with picture

clojure.zip/zipper 則是用來將資料包裝起來,這樣我們就可以透過 clojure.zip 來處理這些樹狀資料

(def zp (clojure.zip/zipper vector? seq (fn [_ c] c)
                            [[1 2 3] [:a :b] 2 3 [40 50 60]]))
;; => [[[1 2 3] [:a :b] 2 3 [40 50 60]] nil]


(clojure.zip/down zp)
;; =>
[[1 2 3]
 {:l [],
  :pnodes [[[1 2 3] [:a :b] 2 3 [40 50 60]]],
  :ppath nil,
  :r ([:a :b] 2 3 [40 50 60])}]


(-> zp
    clojure.zip/down
    first)
;; =>
[1 2 3]


(-> zp
    clojure.zip/down
    clojure.zip/right)
;; =>
[[:a :b]
 {:l [[1 2 3]],
  :pnodes [[[1 2 3] [:a :b] 2 3 [40 50 60]]],
  :ppath nil,
  :r (2 3 [40 50 60])}]