原文地址:https://github.com/bbatsov/clojure-style-guide
这份Clojure代码规范旨在提供一系列的最佳实践,让现实工作中的Clojure程序员能够写出易于维护的代码,并能与他人协作和共享。一份反应真实需求的代码规范才能被人接收,而那些理想化的、甚至部分观点遭到程序员拒绝的代码规范注定不会长久——无论它有多出色。
这份规范由多个章节组成,每个章节包含一组相关的规则。我会尝试去描述每条规则背后的理念(过于明显的理念我就省略了)。
这些规则并不是我凭空想象的,它们出自于我作为一个专业软件开发工程师长久以来的工作积累,以及Clojure社区成员们的反馈和建议,还有各种广为流传的Clojure编程学习资源,如《Clojure Programming》、《The Joy of Clojure》等。
这份规范还处于编写阶段,部分章节有所缺失,内容并不完整;部分规则没有示例,或者示例还不能完全将其描述清楚。未来这些问题都会得到改进,只是请你了解这一情况。
你可以使用Transmuter生成一份本规范的PDF或HTML格式的文档。
目录
源代码的布局和组织结构
几乎所有人都认为任何代码风格都是丑陋且难以阅读的,除了自己的之外。把这句话中的“除了自己之外”去掉,那差不多就能成立了。
—— Jerry Coffin 关于代码缩进的评论
1 2 3 4 5 6 7
| (when something (something-else))
(when something (something-else))
|
1 2 3 4 5 6 7
| (filter even? (range 1 10))
(filter even? (range 1 10))
|
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})
|
- 当
defn
没有文档字符串时,可以选择省略函数名和参数列表之间的空行。
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))
|
- 当函数体较简短时,可以选择忽略参数列表和函数体之间的空行。
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))
(defn goo [x] (bar x))
(defn foo ([x] (bar x)) ([x y] (if (predicate? x) (bar x) (baz x))))
(defn foo [x] (if (predicate? x) (bar x) (baz x)))
|
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风格的换行符(*BSD、Solaris、Linux、OSX用户无需设置,Windows用户则需要格外注意了)
- 如果你使用Git,为了防止项目中意外引入Windows风格的换行符,不妨添加如下设置:
1
| $ git config --global core.autocrlf true
|
- 在括号
(
、{
、[
、]
、}
、)
的外部添加空格,括号内部不要添加。
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中适当使用逗号和换行以增强可读性。
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) )
|
1 2 3 4 5 6 7 8
| (def x ...)
(defn foo ...)
(def x ...) (defn foo ...)
|
- 函数或宏的定义体中不要添加空行。
- 每行尽量不超过80个字符。
- 避免在行末输入多余的空格。
- 为每个命名空间创建单独的文件。
- 使用一个完整的
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)
|
语法
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!")))
|
1 2 3 4
| (defn foo [] (def x 5) ...)
|
- 本地变量名不应覆盖
clojure.core
中定义的函数:
- 使用
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)))
|
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)))
|
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))
|
1 2 3 4 5
| (not= foo bar)
(not (= foo bar))
|
1 2 3 4 5
| #(Math/round %)
#(Math/round %1)
|
1 2 3 4 5
| #(Math/pow %1 %2)
#(Math/pow % %2)
|
1 2 3 4 5
| (filter even? (range 1 10))
(filter #(even? %) (range 1 10))
|
- 当匿名函数包含多行语句时,使用
fn
来定义,而非#(do ...)
。
1 2 3 4 5 6 7 8
| (fn [x] (println x) (* x 2))
#(do (println %) (* % 2))
|
- 在特定情况下优先使用
complement
,而非匿名函数。
1 2 3 4 5
| (filter (complement some-pred?) coll)
(filter #(not (some-pred? %)) coll)
|
当函数已存在对应的求反函数时,则应使用该求反函数(如even?
和odd?
)。
1 2 3 4 5
| (map #(capitalize (trim %)) ["top " " test "])
(map (comp capitalize trim) ["top " " test "])
|
1 2 3 4 5
| (map #(+ 5 %) (range 1 10))
(map (partial + 5) (range 1 10))
|
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)))
|
- 当需要连续调用Java类的方法时,优先使用
..
,而非->
。
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"))
|
- 当比较的变量和方式相同时,优先使用
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)
|
- 当条件是常量时,优先使用
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)
|
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)
。
进行Java操作时,优先使用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
|
(java.util.ArrayList. 100)
(new java.util.ArrayList 100)
(Math/pow 2 10)
(. Math pow 2 10)
(.substring "hello" 1 3)
(. "hello" substring 1 3)
Integer/MAX_VALUE
(. Integer MAX_VALUE)
(.someField some-object)
(. some-object some-field)
|
命名
编程中真正的难点只有两个:验证缓存的有效性;命名。
—— Phil Karlton
- 命名空间建议使用以下两种方式:
- 对于命名空间中较长的元素,使用
lisp-case
格式,如bruce.project-euler
。
- 使用
lisp-case
格式来命名函数和变量。
- 使用
CamelCase
来命名接口(protocol)、记录(record)、结构和类型(struct & type)。对于HTTP、RFC、XML等缩写,仍保留其大写格式。
- 对于返回布尔值的函数名称,使用问号结尾,如
even?
。
- 当方法或宏不能在STM中安全使用时,须以感叹号结尾,如
reset!
。
- 命名类型转换函数时使用
->
,而非to
。
1 2 3 4 5
| (defn f->c ...)
(defn f-to-c ...)
|
- 对于可供重绑定的变量(即动态变量),使用星号括起,如
*earmuffs*
。
- 无需对常量名进行特殊的标识,因为所有的变量都应该是常量,除非有特别说明。
- 对于解构过程中或参数列表中忽略的元素,使用
_
来表示。
- 参考
clojure.core
中的命名规范,如pred
、coll
:
- 函数:
f
、g
、h
:参数内容是一个函数
n
:整数,通常是一个表示大小的值
index
:整数索引
x
、y
:数值
s
:字符串
coll
:集合
pred
:断言型的闭包
& more
:可变参数
- 宏:
expr
:表达式
body
:语句
binding
:一个向量,包含宏的绑定
集合
用100种函数去操作同一种数据结构,要好过用10种函数操作10种数据结构。
—— Alan J. Perlis
- 避免使用列表(list)来存储数据(除非它真的就是你想要的)。
- 优先使用关键字(keyword),而非普通的哈希键:
1 2 3 4 5
| {:name "Bruce" :age 30}
{"name" "Bruce" "age" 30}
|
- 编写集合时,优先使用内置的语法形式,而非构造函数。但是,在定义唯一值集合(set)时,只有当元素都是常量时才可使用内置语法,否则应使用构造函数,如下所示:
1 2 3 4 5 6 7 8 9
| [1 2 3] #{1 2 3} (hash-set (func1) (func2))
(vector 1 2 3) (hash-set 1 2 3) #{(func1) (func2)}
|
避免使用数值索引来访问集合元素。
优先使用关键字来获取哈希表(map)中的值。
1 2 3 4 5 6 7 8 9 10
| (def m {:name "Bruce" :age 30})
(:name m)
(get m :name)
(m :name)
|
1 2 3 4
| (filter #{\a \e \o \i \u} "this is a test")
|
1
| ((juxt :a :b) {:a "ala" :b "bala"})
|
可变量
引用(Refs)
- 建议所有的IO操作都使用
io!
宏进行包装,以免不小心在事务中调用了这些代码。
- 避免使用
ref-set
。
- 控制事务的大小,即事务所执行的工作越少越好。
- 避免出现短期事务和长期事务访问同一个引用(Ref)的情形。
代理(Agents)
send
仅使用于计算密集型、不会因IO等因素阻塞的线程。
send-off
则用于会阻塞、休眠的线程。
原子(Atoms)
字符串
- 优先使用
clojure.string
中提供的字符串操作函数,而不是Java中提供的或是自己编写的函数。
1 2 3 4 5
| (clojure.string/upper-case "bruce")
(.toUpperCase "bruce")
|
异常
- 复用已有的异常类型,如:
java.lang.IllegalArgumentException
java.lang.UnsupportedOperationException
java.lang.IllegalStateException
java.io.IOException
- 优先使用
with-open
,而非finally
。
宏
- 如果可以用函数实现相同功能,不要编写一个宏。
- 首先编写一个宏的用例,尔后再编写宏本身。
- 尽可能将一个复杂的宏拆解为多个小型的函数。
- 宏只应用于简化语法,其核心应该是一个普通的函数。
- 使用语法转义(syntax-quote,即反引号),而非手动构造
list
。
注释
好的代码本身就是文档。因此在添加注释之前,先想想自己该如何改进代码,让它更容易理解。做到这一点后,再通过注释让代码更清晰。
——Steve McConnel
1 2 3 4 5 6 7 8 9 10 11 12
|
(defn fnord [zarquon] (quux zot mumble frotz))
|
- 注释要和代码同步更新。过期的注释还不如没有注释。
- 有时,使用
#_
宏要优于普通的注释:
1 2 3 4 5 6 7
| (+ foo #_(bar x) delta)
(+ foo delta)
|
好的代码和好的笑话一样,不需要额外的解释。
——Russ Olsen
- 避免使用注释去描述一段写得很糟糕的代码。重构它,让它更为可读。(做或者不做,没有尝试这一说。——Yoda)
注释中的标识
- 标识应该写在对应代码的上一行。
- 标识后面是一个冒号和一个空格,以及一段描述文字。
- 如果标识的描述文字超过一行,则第二行需要进行缩进。
- 将自己姓名的首字母以及当前日期附加到标识描述文字中:
1 2 3 4 5
| (defn some-fun [] (baz))
|
- 对于功能非常明显,实在无需添加注释的情况,可以在行尾添加一个标识:
1 2 3
| (defn bar [] (sleep 100))
|
- 使用
TODO
来表示需要后期添加的功能或特性。
- 使用
FIXME
来表示需要修复的问题。
- 使用
OPTIMIZE
来表示会引起性能问题的代码,并需要修复。
- 使用
HACK
来表示这段代码并不正规,需要在后期进行重构。
- 使用
REVIEW
来表示需要进一步审查这段代码,如:REVIEW: 你确定客户会正确地操作X吗?
- 可以使用其它你认为合适的标识关键字,但记得一定要在项目的
README
文件中描述这些自定义的标识。
惯用法
- 使用函数式风格进行编程,避免改变变量的值。
- 保持编码风格。
- 用正常人的思维来思考。
贡献
本文中的所有内容都还没有最后定型,我很希望能够和所有对Clojure代码规范感兴趣的同仁一起编写此文,从而形成一份对社区有益的文档。
你可以随时创建讨论话题,或发送合并申请。我在这里提前表示感谢。
宣传
一份由社区驱动的代码规范如果得不到社区本身的支持和认同,那它就毫无意义了。发送一条推特,向朋友和同事介绍此文。任何评论、建议、以及意见都能够让我们向前迈进一小步。请让我们共同努力吧!