程式一致地使用慣用寫法 (idioms) ,一方面可以有效連結意圖與程式碼結構,一方面也可以減少讀者日後讀程式碼時的驚訝與困擾。底下我把慣用寫法分成三大類:
- 么半群 (monoid)
- 提高可讀性
- 特定問題的慣用寫法
么半群
什麼是么半群? 一個運算子可以放進 reduce
來使用,無論是 0 個、1 個、2 個參數傳入,它都有定義的話,就很像是么半群。
在 Clojure 裡,許多運算子,都算是「么半群」或是擁有近似於「么半群」的性質。比方說:
(+) ;; => 0
(+ 1) ;; => 1
(conj) ;; => []
(conj 1) ;; => [1]
(concat) ;; => ()
(concat [1]) ;; => (1)
(set/union) ;; => #{}
(set/union #{1}) ;; => #{1}
(merge) ;; => nil
(merge {:a 1}) ;; => {:a 1}
也因此,如果定義了一個有『累積』特性的函數,應該考慮也定義它在只有 0 個或是 1 個參數傳入時的行為,讓這個函數變成么半群。
提高可讀性
- 刻意加上
do block
以暗示某程式碼區塊內有副作用 (side effect) - 不要在 Java interop 時,使用
..
運算子,因為『不尋常的程式碼就應該看起來比較不尋常。』 nil
可能有多種不同的意涵,比方說:沒有真值、沒有序列、空的聚集等。所以大量地使用nil
時,要適度地搭配使用自行定義的關鍵字來表達語意,比方說,也同時使用:not-found
來減少模糊性。
特定問題的慣用寫法
下列的四個問題, Clojure 語言有提供一些剛好適用的慣用寫法或是語法。
- 底層的 API 突然多了一個參數 ---
binding
- 需要處理可變的狀態 (state) ---
atom
- 交互遞迴 (mutual recursion) ---
letfn
- 笛卡爾積 (cartesian product) ---
for
底層的 API 突然多了一個參數
如下的問題
(defn a [x]
(b x))
(defn b [x]
(c x))
(defn c [x]
(library/compute x))
有一天如果 library/compute
的參數新增了一個,那程式碼該怎麼改、最省事呢?可以使用 binding
。
(def ^:dynamic *turbo-mode?* true)
(defn a
([x]
(b x))
([x turbo-mode?]
(binding [*turbo-mode?* turbo-mode?]
(b x))))
(defn b [x]
(c x))
(defn c [x]
(library/compute x *turbo-mode?*))
需要處理可變的狀態 (mutable state)
Clojure 有提供 atom
, ref
, agent
三種不同的結構來處理「可變的狀態」 (mutable state) 。在上述三種語法中, atom
最容易使用。 ref
和 agent
只有在某些極端需要超高效能的時候才值得使用。
交互遞迴
letfn
的寫法與一般的 let
有頗大的差異。隨便使用的話,反而大幅增加程式碼閱讀的困難。交互遞迴才是使用 letfn
的最佳時機。
笛卡爾積
for
最適合處理的問題,就是 cartesian product
,因為 for
有宣告式的語法。
範例
[:html
[:ul
(for [item todo-items]
[:li item])]]