榜样很重要。
– 《机械战警》 Alex J. Murphy 警官
这份 Clojure 旨在为 Clojure 程序员编写简洁易懂,易于维护的高质量 Clojure 代码提供一份最佳实践。无论多么好的风格或指南,但是过于理想化的结果导致大家拒绝使用或者可能根本没人用,毫无意义。
本指南分为几个小节,每一小节由几条相关的规则构成。我尽力在每条规则后面说明理由(如果省略了说明,那是因为其理由显而易见)。
这些规则不是我凭空想象出来的 — 它们中的绝大部分来自我多年以来作为职业软件工程师的经验,来自 Clojure 社区成员的反馈和建议,以及许多备受推崇的 Clojure 编程资源,例如 "Clojure Programming" 和 "The Joy of Clojure"。
这份指南还处于不断完善中,可能有一些部分是缺失的,可能有一些是不完善的,可能有一些规则缺少例子,可能有一些规例没有用例子阐述的足够清晰。要记住的是,随着时间的推移这些问题都会被解决。
请注意,Clojure 开发者社区同样维护了一份 coding standards for libraries 列表。
所有风格都又丑又难读,自己的除外。几乎人人都这样想。把 “自己的除外” 拿掉,
他们或许是对的...
– Jerry Coffin(论缩排)
form
的内容。 包括所有的 def
form
, 特殊 form
(special form
),以及引入局域绑定的宏 (例如 loop
, let
, when-let
) 和例如 when
, cond
, as->
,cond->
,case
,with-*
的宏。 [link] ;; good
(when something
(something-else))
(with-out-str
(println "Hello, ")
(println "world!"))
;; bad - four spaces
(when something
(something-else))
;; bad - one space
(with-out-str
(println "Hello, ")
(println "world!"))
;; good
(filter even?
(range 1 10))
;; bad
(filter even?
(range 1 10))
;; good
(filter
even?
(range 1 10))
(or
ala
bala
portokala)
;; bad - two-space indent
(filter
even?
(range 1 10))
(or
ala
bala
portokala)
let
绑定以及 map
关键字缩排在同一层级。 [link] ;; good
(let [thing1 "some stuff"
thing2 "other stuff"]
{:thing1 thing1
:thing2 thing2})
;; bad
(let [thing1 "some stuff"
thing2 "other stuff"]
{:thing1 thing1
:thing2 thing2})
defn
没有 docstring
时,函数名称和参数列表可以选择性的分布在同一行。 [link] ;; good
(defn foo
[x]
(bar x))
;; good
(defn foo [x]
(bar x))
;; bad
(defn foo
[x] (bar x))
multimethod
的 dispatch-val
要和函数名称保持在同一行。 [link] ;; good
(defmethod foo :bar [x] (baz x))
(defmethod foo :bar
[x]
(baz x))
;; bad
(defmethod foo
:bar
[x]
(baz x))
(defmethod foo
:bar [x]
(baz x))
docstring
的时候,尤其是对于使用这个 docstring
的函数, 注意正确的位置应当是函数名称之后,而不是参数列表之后。 后者没有语法错误并且不会引发异常, 但是并没有作为文档绑定到函数名称对应的 var
, 仅仅是成为了函数内容的一部分。 [link] ;; good
(defn foo
"docstring"
[x]
(bar x))
;; bad
(defn foo [x]
"docstring"
(bar x))
;; good
(defn foo [x]
(bar x))
;; good for a small function body
(defn foo [x] (bar x))
;; good for multi-arity functions
(defn foo
([x] (bar x))
([x y]
(if (predicate? x)
(bar x)
(baz x))))
;; bad
(defn foo
[x] (if (predicate? x)
(bar x)
(baz x)))
;; good
(defn foo
"I have two arities."
([x]
(foo x 1))
([x y]
(+ x y)))
;; bad - extra indentation
(defn foo
"I have two arities."
([x]
(foo x 1))
([x y]
(+ x y)))
fold
实现。 [link] ;; good - it's easy to scan for the nth arity
(defn foo
"I have two arities."
([x]
(foo x 1))
([x y]
(+ x y)))
;; okay - the other arities are applications of the two-arity
(defn foo
"I have two arities."
([x y]
(+ x y))
([x]
(foo x 1))
([x y z & more]
(reduce foo (foo x (foo y z)) more)))
;; bad - unordered for no apparent reason
(defn foo
([x] 1)
([x y z] (foo x (foo y z)))
([x y] (+ x y))
([w x y z & more] (reduce foo (foo w (foo x (foo y z))) more)))
docstring
的每一行。 [link] ;; good
(defn foo
"Hello there. This is
a multi-line docstring."
[]
(bar))
;; bad
(defn foo
"Hello there. This is
a multi-line docstring."
[]
(bar))
bash$ git config --global core.autocrlf true
(
, {
和 [
) 或者前面紧跟着右括号 ()
, }
and ]
), 使用空格进行分隔。 相反地, 左括号的右边和右括号的左边忽略空格。 [link] ;; good
(foo (bar baz) quux)
;; bad
(foo(bar baz)quux)
(foo ( bar baz ) quux)
语法糖会导致分号癌。
– Alan Perlis
;; good
[1 2 3]
(1 2 3)
;; bad
[1, 2, 3]
(1, 2, 3)
map
字面量中适当的使用括号,可以提高 map
的可读性。 [link] ;; good
{:name "Bruce Wayne" :alter-ego "Batman"}
;; good and arguably a bit more readable
{:name "Bruce Wayne"
:alter-ego "Batman"}
;; good and arguably more compact
{:name "Bruce Wayne", :alter-ego "Batman"}
;; good; single line
(when something
(something-else))
;; bad; distinct lines
(when something
(something-else)
)
form
之间使用空行分隔。 [link] ;; good
(def x ...)
(defn foo ...)
;; bad
(def x ...)
(defn foo ...)
这个规则的一个例外是,将相关的 def
放在一起。
;; good
(def min-rows 10)
(def max-rows 20)
(def min-cols 15)
(def max-cols 30)
let
, cond
。 [link]ns
form
来定义命名空间, 包含 refer
, require
, import
,并且按照前面的顺序。 [link] (ns examples.ns
(:refer-clojure :exclude [next replace remove])
(:require [clojure.string :as s :refer [blank?]]
[clojure.set :as set]
[clojure.java.shell :as sh])
(:import java.util.Date
java.text.SimpleDateFormat
[java.util.concurrent Executors
LinkedBlockingQueue]))
ns
form
中,使用 :require :as
优于 :require :refer
, :require :refer
优于 :refer :all
,不建议使用 :use
。 [link] ;; good
(ns examples.ns
(:require [clojure.zip :as zip]))
;; good
(ns examples.ns
(:require [clojure.zip :refer [lefts rights]))
;; acceptable as warranted
(ns examples.ns
(:require [clojure.zip :refer :all]))
;; bad
(ns examples.ns
(:use clojure.zip))
;; good
(ns example.ns)
;; bad
(ns example)
require
和 refer
等命名空间操作函数, 在 REPL
的环境之外,这些函数是完全没有必要的。 [link]declare
声明向前引用。 [link]map
和 loop/recur
等高阶函数。 [link] ;; good
(defn foo [x]
{:pre [(pos? x)]}
(bar x))
;; bad
(defn foo [x]
(if (pos? x)
(bar x)
(throw (IllegalArgumentException. "x must be a positive number!")))
var
。 [link] ;; very bad
(defn foo []
(def x 5)
...)
clojure.core
中的命名。 [link] ;; bad - you're forced to use clojure.core/map fully qualified inside
(defn foo [map]
...)
alter-var-root
代替 def
修改 var
的值。 [[link]](#alter-var) ;; good
(def thing 1) ; value of thing is now 1
; do some stuff with thing
(alter-var-root #'thing (constantly nil)) ; value of thing is now nil
;; bad
(def thing 1)
; do some stuff with thing
(def thing nil)
; value of thing is now nil
seq
作为终止条件去测试序列是否为空 (这种技术通常被称为 nil punning)。 [link] ;; good
(defn print-seq [s]
(when (seq s)
(prn (first s))
(recur (rest s))))
;; bad
(defn print-seq [s]
(when-not (empty? s)
(prn (first s))
(recur (rest s))))
sequence
) 转换为矢量 (vector
) 时,使用 vec
优于 into
。 [link] ;; good
(vec some-seq)
;; bad
(into [] some-seq)
when
代替 (if ... (do ...))
。 [link] ;; good
(when pred
(foo)
(bar))
;; bad
(if pred
(do
(foo)
(bar)))
if-let
代替 let
+ if
。 [link] ;; good
(if-let [result (foo x)]
(something-with result)
(something-else))
;; bad
(let [result (foo x)]
(if result
(something-with result)
(something-else)))
when-let
代替 let
+ when
。 [link] ;; good
(when-let [result (foo x)]
(do-something-with result)
(do-something-more-with result))
;; bad
(let [result (foo x)]
(when result
(do-something-with result)
(do-something-more-with result)))
if-not
代替 (if (not ...) ...)
。 [link] ;; good
(if-not pred
(foo))
;; bad
(if (not pred)
(foo))
when-not
代替 (when (not ...) ...)
。 [link];; good
(when-not pred
(foo)
(bar))
;; bad
(when (not pred)
(foo)
(bar))
when-not
代替 (if-not ... (do ...))
。 [link];; good
(when-not pred
(foo)
(bar))
;; bad
(if-not pred
(do
(foo)
(bar)))
not=
代替 (not (= ...))
。 [link];; good
(not= foo bar)
;; bad
(not (= foo bar))
printf
代替 (print (format) ...)
。 [link];; good
(printf "Hello, %s!\n" name)
;; ok
(println (format "Hello, %s!" name))
<
, >
等,可以接受多个参数。 [link];; good
(< 5 x 10)
;; bad
(and (> x 5) (< x 10))
%
优于 %1
。 [link];; good
#(Math/round %)
;; bad
#(Math/round %1)
%1
优于 %
。 [link];; good
#(Math/pow %1 %2)
;; bad
#(Math/pow % %2)
;; good
(filter even? (range 1 10))
;; bad
(filter #(even? %) (range 1 10))
form
时,不要使用函数字面量。 [link];; good
(fn [x]
(println x)
(* x 2))
;; bad (you need an explicit do form)
#(do (println %)
(* % 2))
complement
而不是匿名函数。 [link];; good
(filter (complement some-pred?) coll)
;; bad
(filter #(not (some-pred? %)) coll)
如果反向谓词作为一个独立函数存在时(例如,<code>event?</code> 和 <code>odd?</code>),
这条规则可以忽略。
comp
让代码变得简洁。 [link];; Assuming `(:require [clojure.string :as str])`...
;; good
(map #(str/capitalize (str/trim %)) ["top " " test "])
;; better
(map (comp str/capitalize str/trim) ["top " " test "])
partial
让代码变得简洁。 [link];; good
(map #(+ 5 %) (range 1 10))
;; (arguably) better
(map (partial + 5) (range 1 10))
form
深度嵌套式,使用 threading
宏 ->
(thread-first
) 和 ->>
(thread-last
)。 [link];; good
(-> [1 2 3]
reverse
(conj 4)
prn)
;; not as good
(prn (conj (reverse [1 2 3])
4))
;; good
(->> (range 1 10)
(filter even?)
(map (partial * 2)))
;; not as good
(map (partial * 2)
(filter even? (range 1 10)))
cond
中使用 :else
捕获所有没有匹配的表达式。 [link];; good
(cond
(< n 0) "negative"
(> n 0) "positive"
:else "zero"))
;; bad
(cond
(< n 0) "negative"
(> n 0) "positive"
true "zero"))
condp
优于 cond
。 [link];; good
(cond
(= x 10) :ten
(= x 20) :twenty
(= x 30) :thirty
:else :dunno)
;; much better
(condp = x
10 :ten
20 :twenty
30 :thirty
:dunno)
case
优于 cond
和 condp
。 [link];; good
(cond
(= x 10) :ten
(= x 20) :twenty
(= x 30) :forty
:else :dunno)
;; better
(condp = x
10 :ten
20 :twenty
30 :forty
:dunno)
;; best
(case x
10 :ten
20 :twenty
30 :forty
:dunno)
cond
及相关的宏中,使用简短的 form
, 否则应该使用注释或者空行进行分组来进行视觉上的提示。 [link];; good
(cond
(test1) (action1)
(test2) (action2)
:else (default-action))
;; ok-ish
(cond
;; test case 1
(test1)
(long-function-name-which-requires-a-new-line
(complicated-sub-form
(-> 'which-spans multiple-lines)))
;; test case 2
(test2)
(another-very-long-function-name
(yet-another-sub-form
(-> 'which-spans multiple-lines)))
:else
(the-fall-through-default-case
(which-also-spans 'multiple
'lines)))
set
作为谓词。 [link];; good
(remove #{1} [0 1 2 3 4 5])
;; bad
(remove #(= % 1) [0 1 2 3 4 5])
;; good
(count (filter #{\a \e \i \o \u} "mary had a little lamb"))
;; bad
(count (filter #(or (= % \a)
(= % \e)
(= % \i)
(= % \o)
(= % \u))
"mary had a little lamb"))
(inc x)
和 (dec x)
代替 (+ x 1)
和 (- x 1)
。 [link](pos? x)
, (neg? x)
和 (zero? x)
代替 (> x 0)
, (< x 0)
和 (= x 0)
。 [link]list*
代替一系列嵌套 cons
调用。 [link];; good
(list* 1 2 3 [4 5])
;; bad
(cons 1 (cons 2 (cons 3 [4 5])))
java
语法糖 form
。 [link];;; object creation
;; good
(java.util.ArrayList. 100)
;; bad
(new java.util.ArrayList 100)
;;; static method invocation
;; good
(Math/pow 2 10)
;; bad
(. Math pow 2 10)
;;; instance method invocation
;; good
(.substring "hello" 1 3)
;; bad
(. "hello" substring 1 3)
;;; static field access
;; good
Integer/MAX_VALUE
;; bad
(. Integer MAX_VALUE)
;;; instance field access
;; good
(.someField some-object)
;; bad
(. some-object someField)
metadata
槽中的元素仅仅是键为 keyword
, 值为布尔值 true
的键值对时,使用 metadata
的简写形式。 [link];; good
(def ^:private a 5)
;; bad
(def ^{:private true} a 5)
;; good
(defn- private-fun [] ...)
(def ^:private private-var ...)
;; bad
(defn private-fun [] ...) ; not private at all
(defn ^:private private-fun [] ...) ; overly verbose
(def private-var ...) ; not private at all
@#'some.ns/var
形式的 form
访问私有 var
(例如,进行测试) 。 [link]metadata
的正确附加对象。 [link];; we attach the metadata to the var referenced by `a`
(def ^:private a {})
(meta a) ;=> nil
(meta #'a) ;=> {:private true}
;; we attach the metadata to the empty hash-map value
(def a ^:private {})
(meta a) ;=> {:private true}
(meta #'a) ;=> nil
程序设计的真正难题是替事物命名以及缓存失效。
– Phil Karlton
project.module
organization.project.module
Lisp
小写 (lisp-case
) (例如,bruce.project-euler
)。 [link]Lisp
小写 (lisp-case
)。 [link];; good
(def some-var ...)
(defn some-fun ...)
;; bad
(def someVar ...)
(defn somefun ...)
(def some_fun ...)
protocols
),纪录 (records
),结构 (structs
), 和类型 (types
), 使用驼峰式大小写(CamelCase
) (HTTP、RFC、XML 等首字母缩写应该仍旧保持大写形式)。 [link]even?
)。 [link];; good
(defn palindrome? ...)
;; bad
(defn palindrome-p ...) ; Common Lisp style
(defn is-palindrome ...) ; Java style
STM
事务中非安全的方法或宏,名字以感叹号结尾 (例如,reset!
) 。 [link]->
代替 to
。 [link];; good
(defn f->c ...)
;; not so good
(defn f-to-c ...)
*earmuffs*
为要重新绑定事物命名 (例如,动态全局变量)。 [link];; good
(def ^:dynamic *a* 10)
;; bad
(def ^:dynamic a 10)
destructure targets
) 和形式参数 (formal argument
),使用 _
进行命名。 [link];; good
(let [[a b _ c] [1 2 3 4]]
(println a b c))
(dotimes [_ 3]
(println "Hello!"))
;; bad
(let [[a b c d] [1 2 3 4]]
(println a b d))
(dotimes [i 3]
(println "Hello!"))
clojure.core
中示例的惯例,例如,pred
和 coll
,进行命名。 [link]f
, g
, h
- 函数输入n
- 整数输入,通常代表大小index
, i
- 整数索引x
, y
- 数字xs
- 序列m
- 映射s
- 字符串输入re
- 正则表达式coll
- 集合pred
- 谓词闭包& more
- 变长参数xf
- xform, a transducer 在宏中:
* `expr` - 表达式
* `body` - 宏定义
* `binding` - 宏绑定矢量
一百个函数去操作一个数据结构要优于十个函数去操作十个数据结构。
– Alan J. Perlis
lists
) 保存常用数据结构 (除非真的需要列表) 。 [link]keywords
) 作为哈希键。 [link];; good
{:name "Bruce" :age 30}
;; bad
{"name" "Bruce" "age" 30}
set
) 的时候, 如果值是编译时常量,只使用字面量语法。 [link] ;; good
[1 2 3]
#{1 2 3}
(hash-set (func1) (func2)) ; values determined at runtime
;; bad
(vector 1 2 3)
(hash-set 1 2 3)
#{(func1) (func2)} ; will throw runtime exception if (func1) = (func2)
index
) 获取集合 (collection
) 的元素。 [link]maps
) 的值。 [link] (def m {:name "Bruce" :age 30})
;; good
(:name m)
;; more verbose than necessary
(get m :name)
;; bad - susceptible to NullPointerException
(m :name)
;; good
(filter #{\a \e \o \i \u} "this is a test")
;; bad - too ugly to share
((juxt :a :b) {:a "ala" :b "bala"})
transient collections
),除非代码对性能有要求。 [link]Java
集合。 [link]Java
数组,除了和 Java
互操作的场景, 或者大量原始类型 (primitive types
) 操作的性能关键部分。 [link]I/O
操作包裹到 io!
宏 (macro
) 中, 以防不小心在事务 (transaction
) 中调用产生意外。 [link]ref-set
。 [link] (def r (ref 0))
;; good
(dosync (alter r + 5))
;; bad
(dosync (ref-set r 5))
transactions
) 小而紧凑 (事务中的逻辑) 。 [link]Ref
。 [link]CPU
绑定 (CPU bound
), 或者没有 I/O
阻塞 (block on I/O
) 的时候使用 send
。 [link]send-off
,或者以其它的方式配合线程。 [link]STM
事务 (STM transactions
) 中更新原子 (atom
)。 [link]swap!
而不是 reset!
。 [link] (def a (atom 0))
;; good
(swap! a + 5)
;; not as good
(reset! a 5)
clojure.string
中的函数操作字符串,优于 Java
互操作 (Java interop
) 或者自定义函数。 [link] ;; good
(clojure.string/upper-case "bruce")
;; bad
(.toUpperCase "bruce")
Clojure
代码 — 当抛出异常时 — 会抛出标准异常类型 (例如, java.lang.IllegalArgumentException
, java.lang.UnsupportedOperationException
, java.lang.IllegalStateException
, java.io.IOException
)。 [link]with-open
优于 finally
。 [link]forms
(syntax-quoted forms
) 优于自己手动构建列表。 [link]良好的代码自身就是最佳的文档。当你要添加一个注释时, 扪心自问,“如何改善代码让它不需要注释?” 改善代码,再写相应文档使之更清楚。
– Steve McConnell
;;;; Frob Grovel
;;; This section of code has some important implications:
;;; 1. Foo.
;;; 2. Bar.
;;; 3. Baz.
(defn fnord [zarquon]
;; If zob, then veeblefitz.
(quux zot
mumble ; Zibblefrotz.
frotz))
;; bad
(inc counter) ; increments counter by one
form
的时候,使用读取宏 #_
优于普通的注释。 [link] ;; good
(+ foo #_(bar x) delta)
;; bad
(+ foo
;; (bar x)
delta)
好的代码就像是好的笑话 —— 它不需要解释。
– Russ Olsen
(defn some-fun
[]
;; FIXME: This has crashed occasionally since v1.2.3. It may
;; be related to the BarBazUtil upgrade. (xz 13-1-31)
(baz))
(defn bar
[]
(sleep 100)) ; OPTIMIZE
TODO
标记应当加入的特征与功能。 [link]FIXME
标记需要修复的代码。 [link]OPTIMIZE
标记可能引发性能问题的低效代码。 performance problems. [link]HACK
标记代码异味,即那些应当被重构的可疑编码习惯。 [link]REVIEW
标记需要确认与编码意图是否一致的可疑代码。 比如,REVIEW: Are we sure this is how the client does X currently?
。 [link]README
或类似文档中予以说明。 [link]下面是一些 Clojure 社区创建的工具,为你写出地道的 Clojure 助一臂之力。
ns
声明的工具。test/yourproject/
(而不是 src/yourproject/
)。 构建工具保证了在需要它们的上下文中是可用的, 大多数模版会自动完成这些功能。 [link]yourproject.something-test
, 对于的文件通常为 test/yourproject/something_test.clj
(或者 .cljc
, cljs
)。 [link]clojure.test
时, 使用 deftest
定义测试,并且命名为 something-test
,例如: ;; good
(deftest something-test ...)
;; bad
(deftest something-tests ...)
(deftest test-something ...)
(deftest something ...)
这份指南中的任何规则都不是一成不变的。 我渴望和任何一位对 Clojure 风格指南的伙伴一起工作, 最终创造一份对整个 Clojure 社区都大有裨益的资源。
欢迎发起讨论或提交一个带有改进性质的更新请求。在此提前感谢你的帮助!
你也可以通过 gittip 对此项目提供财务方面的支持。
本指南基于 Creative Commons Attribution 3.0 Unported License 授权许可。
一份社区驱动的风格指南,如果没多少人知道, 对一个社区来说就没有多少用处。微博转发这份指南, 分享给你的朋友或同事。我们得到的每个评价、建议或意见都可以让这份指南变得更好一点。 而我们想要拥有最好的指南,不是吗?
共勉之,
Bozhidar
https://github.com/geekerzp/clojure-style-guide/blob/master/README-zhCN.md
2019-02-14 00:44