Day04 慣用寫法 (idioms)


程式一致地使用慣用寫法 (idioms) ,一方面可以有效連結意圖與程式碼結構,一方面也可以減少讀者日後讀程式碼時的驚訝與困擾。底下我把慣用寫法分成三大類:

  1. 么半群 (monoid)
  2. 提高可讀性
  3. 特定問題的慣用寫法

么半群

什麼是么半群? 一個運算子可以放進 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 個參數傳入時的行為,讓這個函數變成么半群。

提高可讀性

  1. 刻意加上 do block 以暗示某程式碼區塊內有副作用 (side effect)
  2. 不要在 Java interop 時,使用 .. 運算子,因為『不尋常的程式碼就應該看起來比較不尋常。』
  3. nil 可能有多種不同的意涵,比方說:沒有真值、沒有序列、空的聚集等。所以大量地使用 nil 時,要適度地搭配使用自行定義的關鍵字來表達語意,比方說,也同時使用 :not-found 來減少模糊性。

特定問題的慣用寫法

下列的四個問題, Clojure 語言有提供一些剛好適用的慣用寫法或是語法。

  1. 底層的 API 突然多了一個參數 --- binding
  2. 需要處理可變的狀態 (state) --- atom
  3. 交互遞迴 (mutual recursion) --- letfn
  4. 笛卡爾積 (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 最容易使用。 refagent 只有在某些極端需要超高效能的時候才值得使用。

交互遞迴

letfn 的寫法與一般的 let 有頗大的差異。隨便使用的話,反而大幅增加程式碼閱讀的困難。交互遞迴才是使用 letfn 的最佳時機。

笛卡爾積

for 最適合處理的問題,就是 cartesian product ,因為 for 有宣告式的語法。

範例

[:html
  [:ul
    (for [item todo-items]
      [:li item])]]
#idiom #Clojure





Elements of Clojure 一書,談的不只是 Clojure ,而是 senior programmer 了解、卻不易對他人明說的隱性知識 (tacit knowledge)。

留言討論