Clojure 代碼規(guī)范

Clojure 代碼規(guī)范 | Ji ZHANG's Blog
http://shzhangji.com/blog/2013/01/04/clojure-style-guide/

原文地址:https://github.com/bbatsov/clojure-style-guide
這份Clojure代碼規(guī)范旨在提供一系列的最佳實踐,讓現(xiàn)實工作中的Clojure程序員能夠?qū)懗鲆子诰S護(hù)的代碼,并能與他人協(xié)作和共享。一份反應(yīng)真實需求的代碼規(guī)范才能被人接收,而那些理想化的、甚至部分觀點遭到程序員拒絕的代碼規(guī)范注定不會長久——無論它有多出色。
這份規(guī)范由多個章節(jié)組成,每個章節(jié)包含一組相關(guān)的規(guī)則。我會嘗試去描述每條規(guī)則背后的理念(過于明顯的理念我就省略了)。
這些規(guī)則并不是我憑空想象的,它們出自于我作為一個專業(yè)軟件開發(fā)工程師長久以來的工作積累,以及Clojure社區(qū)成員們的反饋和建議,還有各種廣為流傳的Clojure編程學(xué)習(xí)資源,如《Clojure Programming》、《The Joy of Clojure》等。
這份規(guī)范還處于編寫階段,部分章節(jié)有所缺失,內(nèi)容并不完整;部分規(guī)則沒有示例,或者示例還不能完全將其描述清楚。未來這些問題都會得到改進(jìn),只是請你了解這一情況。
你可以使用Transmuter生成一份本規(guī)范的PDF或HTML格式的文檔。
目錄
源代碼的布局和組織結(jié)構(gòu)
語法
命名
注釋注釋中的標(biāo)識

異常
集合
可變量
字符串
正則表達(dá)式

慣用法

源代碼的布局和組織結(jié)構(gòu)
幾乎所有人都認(rèn)為任何代碼風(fēng)格都是丑陋且難以閱讀的,除了自己的之外。把這句話中的“除了自己之外”去掉,那差不多就能成立了?!?Jerry Coffin 關(guān)于代碼縮進(jìn)的評論

使用兩個 空格 進(jìn)行縮進(jìn),不使用制表符。

1
2
3
4
5
6
7

;; 正確
(when something
(something-else))

;; 錯誤 - 四個空格
(when something
(something-else))

縱向?qū)R函數(shù)參數(shù)。

1
2
3
4
5
6
7

;; 正確
(filter even?
(range 1 10))

;; 錯誤
(filter even?
(range 1 10))

對齊let綁定,以及map類型中的關(guān)鍵字。

1
2
3
4
5
6
7
8
9
10
11

;; 正確
(let [thing1 "some stuff"
thing2 "other stuff"]
{:thing1 thing1
:thing2 thing2})

;; 錯誤
(let [thing1 "some stuff"
thing2 "other stuff"]
{:thing1 thing1
:thing2 thing2})

當(dāng)defn
沒有文檔字符串時,可以選擇省略函數(shù)名和參數(shù)列表之間的空行。

1
2
3
4
5
6
7
8
9
10
11
12

;; 正確
(defn foo
[x]
(bar x))

;; 正確
(defn foo [x]
(bar x))

;; 錯誤
(defn foo
[x] (bar x))

當(dāng)函數(shù)體較簡短時,可以選擇忽略參數(shù)列表和函數(shù)體之間的空行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

;; 正確
(defn foo [x]
(bar x))

;; 適合簡單的函數(shù)
(defn goo [x] (bar x))

;; 適合包含多種參數(shù)列表的函數(shù)
(defn foo
([x] (bar x))
([x y]
(if (predicate? x)
(bar x)
(baz x))))

;; 錯誤
(defn foo
[x] (if (predicate? x)
(bar x)
(baz x)))

跨行的文檔說明字符串每行都要縮進(jìn)。

1
2
3
4
5
6
7
8
9
10
11
12
13

;; 正確
(defn foo
"Hello there. This is
a multi-line docstring."
[]
(bar))

;; 錯誤
(defn foo
"Hello there. This is
a multi-line docstring."
[]
(bar))

使用Unix風(fēng)格的換行符(*BSD、Solaris、Linux、OSX用戶無需設(shè)置,Windows用戶則需要格外注意了)如果你使用Git,為了防止項目中意外引入Windows風(fēng)格的換行符,不妨添加如下設(shè)置:

1

$ git config --global core.autocrlf true

在括號(
、{
、[
、]
、}
、)
的外部添加空格,括號內(nèi)部不要添加。

1
2
3
4
5
6

;; 正確
(foo (bar baz) quux)

;; 錯誤
(foo(bar baz)quux)
(foo ( bar baz ) quux)

避免在集合中使用逗號分隔符。

1
2
3
4
5
6
7

;; 正確
[1 2 3]
(1 2 3)

;; 錯誤
[1, 2, 3]
(1, 2, 3)

可以考慮在map中適當(dāng)使用逗號和換行以增強可讀性。

1
2
3
4
5
6
7
8
9

;; 正確
{:name "Bruce Wayne" :alter-ego "Batman"}

;; 正確,且會增強可讀性
{:name "Bruce Wayne"
:alter-ego "Batman"}

;; 正確,且較為緊湊
{:name "Bruce Wayne", :alter-ego "Batman"}

將所有的反括號放在一行中。

1
2
3
4
5
6
7
8

;; 正確
(when something
(something-else))

;; 錯誤
(when something
(something-else)
)

頂層函數(shù)之間空出一行。

1
2
3
4
5
6
7
8

;; 正確
(def x ...)

(defn foo ...)

;; 錯誤
(def x ...)
(defn foo ...)

函數(shù)或宏的定義體中不要添加空行。
每行盡量不超過80個字符。
避免在行末輸入多余的空格。
為每個命名空間創(chuàng)建單獨的文件。
使用一個完整的ns
指令來聲明命名空間,其包含import
、require
、refer
、以及use
。

1
2
3
4
5
6
7
8
9
10

(ns examples.ns
(:refer-clojure :exclude [next replace remove])
(:require (clojure [string :as string]
[set :as set])
[clojure.java.shell :as sh])
(:use (clojure zip xml))
(:import java.util.Date
java.text.SimpleDateFormat
(java.util.concurrent Executors
LinkedBlockingQueue)))

避免使用只有一個元素的命名空間名。

1
2
3
4
5

;; 正確
(ns example.ns)

;; 錯誤
(ns example)

避免使用過長的命名空間(不超過五個元素)。

一個函數(shù)不應(yīng)超過10行代碼。事實上,大多數(shù)函數(shù)應(yīng)保持在5行代碼以內(nèi)。

函數(shù)的參數(shù)個數(shù)不應(yīng)超過三到四個。

語法
避免使用require
、refer
等改變命名空間的函數(shù),它們只應(yīng)在REPL中使用。
使用declare
實現(xiàn)引用傳遞。
優(yōu)先使用map
這類高階函數(shù),而非loop/recur

優(yōu)先使用前置、后置條件來檢測函數(shù)參數(shù)和返回值:

1
2
3
4
5
6
7
8
9
10

;; 正確
(defn foo [x]
{:pre [(pos? x)]}
(bar x))

;; 錯誤
(defn foo [x]
(if (pos? x)
(bar x)
(throw (IllegalArgumentException "x must be a positive number!")))

不要在函數(shù)中定義變量:

1
2
3
4

;; 非常糟糕
(defn foo []
(def x 5)
...)

本地變量名不應(yīng)覆蓋clojure.core
中定義的函數(shù):

1
2
3

;; 錯誤——這樣一來函數(shù)中調(diào)用map時就需要指定完整的命名空間了。
(defn foo [map]
...)

使用seq
來判斷一個序列是否為空(空序列等價于nil)。

1
2
3
4
5
6
7
8
9
10
11

;; 正確
(defn print-seq [s]
(when (seq s)
(prn (first s))
(recur (rest s))))

;; 錯誤
(defn print-seq [s]
(when-not (empty? s)
(prn (first s))
(recur (rest s))))

使用when
替代(if ... (do ...)
。

1
2
3
4
5
6
7
8
9
10

;; 正確
(when pred
(foo)
(bar))

;; 錯誤
(if pred
(do
(foo)
(bar)))

使用if-let
替代let

  • if
    。

1
2
3
4
5
6
7
8
9
10

;; 正確
(if-let [result :foo]
(something-with result)
(something-else))

;; 錯誤
(let [result :foo]
(if result
(something-with result)
(something-else)))

使用when-let
替代let

  • when
    。

1
2
3
4
5
6
7
8
9
10

;; 正確
(when-let [result :foo]
(do-something-with result)
(do-something-more-with result))

;; 錯誤
(let [result :foo]
(when result
(do-something-with result)
(do-something-more-with result)))

使用if-not
替代(if (not ...) ...)

1
2
3
4
5
6
7

;; 正確
(if-not (pred)
(foo))

;; 錯誤
(if (not pred)
(foo))

使用when-not
替代(when (not ...) ...)
。

1
2
3
4
5
6
7
8
9

;; 正確
(when-not pred
(foo)
(bar))

;; 錯誤
(when (not pred)
(foo)
(bar))

使用not=
替代(not (= ...))
。

1
2
3
4
5

;; 正確
(not= foo bar)

;; 錯誤
(not (= foo bar))

當(dāng)匿名函數(shù)只有一個參數(shù)時,優(yōu)先使用%
,而非%1
。

1
2
3
4
5

;; 正確

(Math/round %)

;; 錯誤

(Math/round %1)

當(dāng)匿名函數(shù)有多個參數(shù)時,優(yōu)先使用%1
,而非%
。

1
2
3
4
5

;; 正確

(Math/pow %1 %2)

;; 錯誤

(Math/pow % %2)

只有在必要的時候才使用匿名函數(shù)。

1
2
3
4
5

;; 正確
(filter even? (range 1 10))

;; 錯誤
(filter #(even? %) (range 1 10))

當(dāng)匿名函數(shù)包含多行語句時,使用fn
來定義,而非#(do ...)
。

1
2
3
4
5
6
7
8

;; 正確
(fn [x]
(println x)
(* x 2))

;; 錯誤(你不得不使用do

(do (println %)

(* % 2))

在特定情況下優(yōu)先使用complement
,而非匿名函數(shù)。

1
2
3
4
5

;; 正確
(filter (complement some-pred?) coll)

;; 錯誤
(filter #(not (some-pred? %)) coll)

當(dāng)函數(shù)已存在對應(yīng)的求反函數(shù)時,則應(yīng)使用該求反函數(shù)(如even?
和odd?
)。
某些情況下可以用comp
使代碼更簡潔。

1
2
3
4
5

;; 正確
(map #(capitalize (trim %)) ["top " " test "])

;; 更好
(map (comp capitalize trim) ["top " " test "])

某些情況下可以用partial
使代碼更簡潔。

1
2
3
4
5

;; 正確
(map #(+ 5 %) (range 1 10))

;; 或許更好
(map (partial + 5) (range 1 10))

當(dāng)遇到嵌套調(diào)用時,建議使用->
宏和->>
宏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

;; 正確
(-> [1 2 3]
reverse
(conj 4)
prn)

;; 不夠好
(prn (conj (reverse [1 2 3])
4))

;; 正確
(->> (range 1 10)
(filter even?)
(map (partial * 2)))

;; 不夠好
(map (partial * 2)
(filter even? (range 1 10)))

當(dāng)需要連續(xù)調(diào)用Java類的方法時,優(yōu)先使用..
,而非->
。

1
2
3
4
5

;; 正確
(-> (System/getProperties) (.get "os.name"))

;; 更好
(.. System getProperties (get "os.name"))

在cond
和condp
中,使用:else
來處理不滿足條件的情況。

1
2
3
4
5
6
7
8
9
10
11

;; 正確
(cond
(< n 0) "negative"
(> n 0) "positive"
:else "zero"))

;; 錯誤
(cond
(< n 0) "negative"
(> n 0) "positive"
true "zero"))

當(dāng)比較的變量和方式相同時,優(yōu)先使用condp
,而非cond

1
2
3
4
5
6
7
8
9
10
11
12
13

;; 正確
(cond
(= x 10) :ten
(= x 20) :twenty
(= x 30) :forty
:else :dunno)

;; 更好
(condp = x
10 :ten
20 :twenty
30 :forty
:dunno)

當(dāng)條件是常量時,優(yōu)先使用case
,而非cond
或condp
。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

;; 正確
(cond
(= x 10) :ten
(= x 20) :twenty
(= x 30) :forty
:else :dunno)

;; 更好
(condp = x
10 :ten
20 :twenty
30 :forty
:dunno)

;; 最佳
(case x
10 :ten
20 :twenty
30 :forty
:dunno)

某些情況下,使用set
作為判斷條件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

;; 錯誤
(remove #(= % 0) [0 1 2 3 4 5])

;; 正確
(remove #{0} [0 1 2 3 4 5])

;; 錯誤
(count (filter #(or (= % \a)
(= % \e)
(= % \i)
(= % \o)
(= % \u))
"mary had a little lamb"))

;; 正確
(count (filter #{\a \e \i \o \u} "mary had a little lamb"))

使用(inc x)
和(dec x)
替代(+ x 1)
和(- x 1)
。

使用(pos? x)
、(neg? x)
、以及(zero? x)
替代(> x 0)
、(< x 0)
、和(= x 0)
。

進(jìn)行Java操作時,優(yōu)先使用Clojure提供的語法糖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

;;; 創(chuàng)建對象
;; 正確
(java.util.ArrayList. 100)

;; 錯誤
(new java.util.ArrayList 100)

;;; 調(diào)用靜態(tài)方法
;; 正確
(Math/pow 2 10)

;; 錯誤
(. Math pow 2 10)

;;; 調(diào)用實例方法
;; 正確
(.substring "hello" 1 3)

;; 錯誤
(. "hello" substring 1 3)

;;; 訪問靜態(tài)屬性
;; 正確
Integer/MAX_VALUE

;; 錯誤
(. Integer MAX_VALUE)

;;; 訪問實例屬性
;; 正確
(.someField some-object)

;; 錯誤
(. some-object some-field)

命名
編程中真正的難點只有兩個:驗證緩存的有效性;命名?!?Phil Karlton

命名空間建議使用以下兩種方式:項目名稱.模塊名稱

組織名稱.項目名稱.模塊名稱

對于命名空間中較長的元素,使用lisp-case
格式,如bruce.project-euler
。
使用lisp-case
格式來命名函數(shù)和變量。
使用CamelCase
來命名接口(protocol)、記錄(record)、結(jié)構(gòu)和類型(struct & type)。對于HTTP、RFC、XML等縮寫,仍保留其大寫格式。
對于返回布爾值的函數(shù)名稱,使用問號結(jié)尾,如even?
。
當(dāng)方法或宏不能在STM中安全使用時,須以感嘆號結(jié)尾,如reset!
。
命名類型轉(zhuǎn)換函數(shù)時使用->
,而非to

1
2
3
4
5

;; 正確
(defn f->c ...)

;; 不夠好
(defn f-to-c ...)

對于可供重綁定的變量(即動態(tài)變量),使用星號括起,如earmuffs

無需對常量名進(jìn)行特殊的標(biāo)識,因為所有的變量都應(yīng)該是常量,除非有特別說明。
對于解構(gòu)過程中或參數(shù)列表中忽略的元素,使用_
來表示。
參考clojure.core
中的命名規(guī)范,如pred
、coll
:函數(shù):f
、g
、h
:參數(shù)內(nèi)容是一個函數(shù)
n
:整數(shù),通常是一個表示大小的值
index
:整數(shù)索引
x
、y
:數(shù)值
s
:字符串
coll
:集合
pred
:斷言型的閉包
& more
:可變參數(shù)

宏:expr
:表達(dá)式
body
:語句
binding
:一個向量,包含宏的綁定

集合
用100種函數(shù)去操作同一種數(shù)據(jù)結(jié)構(gòu),要好過用10種函數(shù)操作10種數(shù)據(jù)結(jié)構(gòu)?!?Alan J. Perlis

避免使用列表(list)來存儲數(shù)據(jù)(除非它真的就是你想要的)。
優(yōu)先使用關(guān)鍵字(keyword),而非普通的哈希鍵:

1
2
3
4
5

;; 正確
{:name "Bruce" :age 30}

;; 錯誤
{"name" "Bruce" "age" 30}

編寫集合時,優(yōu)先使用內(nèi)置的語法形式,而非構(gòu)造函數(shù)。但是,在定義唯一值集合(set)時,只有當(dāng)元素都是常量時才可使用內(nèi)置語法,否則應(yīng)使用構(gòu)造函數(shù),如下所示:

1
2
3
4
5
6
7
8
9

;; 正確
[1 2 3]

{1 2 3}

(hash-set (func1) (func2)) ; 元素在運行時確定

;; bad
(vector 1 2 3)
(hash-set 1 2 3)

{(func1) (func2)} ; 若(func1)和(func2)的值相等,則會拋出運行時異常。

避免使用數(shù)值索引來訪問集合元素。

優(yōu)先使用關(guān)鍵字來獲取哈希表(map)中的值。

1
2
3
4
5
6
7
8
9
10

(def m {:name "Bruce" :age 30})

;; 正確
(:name m)

;; 錯誤——太過啰嗦
(get m :name)

;; 錯誤——可能拋出空指針異常
(m :name)

集合可以被用作函數(shù):

1
2
3
4

;; 正確
(filter #{\a \e \o \i \u} "this is a test")

;; 缺點——不夠美觀

關(guān)鍵字可以被用作函數(shù):

1

((juxt :a :b) {:a "ala" :b "bala"})

只有在非常強調(diào)性能的情況下才可使用瞬時集合(transient collection)。

避免使用Java集合。

避免使用Java數(shù)組,除非遇到需要和Java類進(jìn)行交互,或需要高性能地處理基本類型時才可使用。

可變量
引用(Refs)
建議所有的IO操作都使用io!
宏進(jìn)行包裝,以免不小心在事務(wù)中調(diào)用了這些代碼。
避免使用ref-set
。
控制事務(wù)的大小,即事務(wù)所執(zhí)行的工作越少越好。
避免出現(xiàn)短期事務(wù)和長期事務(wù)訪問同一個引用(Ref)的情形。

代理(Agents)
send
僅使用于計算密集型、不會因IO等因素阻塞的線程。
send-off
則用于會阻塞、休眠的線程。

原子(Atoms)
避免在事務(wù)中更新原子。
避免使用reset!
。

字符串
優(yōu)先使用clojure.string
中提供的字符串操作函數(shù),而不是Java中提供的或是自己編寫的函數(shù)。

1
2
3
4
5

;; 正確
(clojure.string/upper-case "bruce")

;; 錯誤
(.toUpperCase "bruce")

異常
復(fù)用已有的異常類型,如:java.lang.IllegalArgumentException

java.lang.UnsupportedOperationException

java.lang.IllegalStateException

java.io.IOException

優(yōu)先使用with-open
,而非finally
。


如果可以用函數(shù)實現(xiàn)相同功能,不要編寫一個宏。
首先編寫一個宏的用例,爾后再編寫宏本身。
盡可能將一個復(fù)雜的宏拆解為多個小型的函數(shù)。
宏只應(yīng)用于簡化語法,其核心應(yīng)該是一個普通的函數(shù)。
使用語法轉(zhuǎn)義(syntax-quote,即反引號),而非手動構(gòu)造list
。

注釋
好的代碼本身就是文檔。因此在添加注釋之前,先想想自己該如何改進(jìn)代碼,讓它更容易理解。做到這一點后,再通過注釋讓代碼更清晰。——Steve McConnel

學(xué)會編寫容易理解的代碼,然后忽略下文的內(nèi)容。真的!

對于標(biāo)題型的注釋,使用至少四個分號起始。

對于頂層注釋,使用三個分號起始。

為某段代碼添加注釋時,使用兩個分號起始,且應(yīng)與該段代碼對齊。

對于行尾注釋,使用一個分號起始即可。

分號后面要有一個空格。

1
2
3
4
5
6
7
8
9
10
11
12

;;;; Frob Grovel

;;; 這段代碼有以下前提:
;;; 1. Foo.
;;; 2. Bar.
;;; 3. Baz.

(defn fnord [zarquon]
;; If zob, then veeblefitz.
(quux zot
mumble ; Zibblefrotz.
frotz))

對于成句的注釋,句首字母應(yīng)該大寫,句與句之間用一個空格分隔。
避免冗余的注釋:

1
2

;; 錯誤
(inc counter) ; counter變量的值加1

注釋要和代碼同步更新。過期的注釋還不如沒有注釋。
有時,使用#_
宏要優(yōu)于普通的注釋:

1
2
3
4
5
6
7

;; 正確
(+ foo #_(bar x) delta)

;; 錯誤
(+ foo
;; (bar x)
delta)

好的代碼和好的笑話一樣,不需要額外的解釋?!猂uss Olsen

避免使用注釋去描述一段寫得很糟糕的代碼。重構(gòu)它,讓它更為可讀。(做或者不做,沒有嘗試這一說?!猋oda)

注釋中的標(biāo)識
標(biāo)識應(yīng)該寫在對應(yīng)代碼的上一行。
標(biāo)識后面是一個冒號和一個空格,以及一段描述文字。
如果標(biāo)識的描述文字超過一行,則第二行需要進(jìn)行縮進(jìn)。
將自己姓名的首字母以及當(dāng)前日期附加到標(biāo)識描述文字中:

1
2
3
4
5

(defn some-fun
[]
;; FIXME: 這段代碼在v1.2.3之后偶爾會崩潰,
;; 這可能和升級BarBazUtil有關(guān)。(xz 13-1-31)
(baz))

對于功能非常明顯,實在無需添加注釋的情況,可以在行尾添加一個標(biāo)識:

1
2
3

(defn bar
[]
(sleep 100)) ; OPTIMIZE

使用TODO
來表示需要后期添加的功能或特性。
使用FIXME
來表示需要修復(fù)的問題。
使用OPTIMIZE
來表示會引起性能問題的代碼,并需要修復(fù)。
使用HACK
來表示這段代碼并不正規(guī),需要在后期進(jìn)行重構(gòu)。
使用REVIEW
來表示需要進(jìn)一步審查這段代碼,如:REVIEW: 你確定客戶會正確地操作X嗎?

可以使用其它你認(rèn)為合適的標(biāo)識關(guān)鍵字,但記得一定要在項目的README
文件中描述這些自定義的標(biāo)識。

慣用法
使用函數(shù)式風(fēng)格進(jìn)行編程,避免改變變量的值。
保持編碼風(fēng)格。
用正常人的思維來思考。

貢獻(xiàn)
本文中的所有內(nèi)容都還沒有最后定型,我很希望能夠和所有對Clojure代碼規(guī)范感興趣的同仁一起編寫此文,從而形成一份對社區(qū)有益的文檔。
你可以隨時創(chuàng)建討論話題,或發(fā)送合并申請。我在這里提前表示感謝。
宣傳
一份由社區(qū)驅(qū)動的代碼規(guī)范如果得不到社區(qū)本身的支持和認(rèn)同,那它就毫無意義了。發(fā)送一條推特,向朋友和同事介紹此文。任何評論、建議、以及意見都能夠讓我們向前邁進(jìn)一小步。請讓我們共同努力吧!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容