Clojure-风格指南
原文:clojure-style-guide
译者:JuanitoFatas
?转载:http://lisp.tw/2013/02/19/clojure-style-guide/
这篇 Clojure 风格指南向你推荐现实世界中的最佳实践,Clojure 程序员如何写出可被别的 Clojure 程序员维护的代码。一份风格指南反映出现实世界中的用法,并带有一个理想,避免已经公认是危险的事物被人继续使用,不管看起来是多么的好。
本指南依照相关规则分成数个小节。我尽力在规则之后说明理由(如果省略的话,我相信理由是显而易见的)。
我没有想到所有的规则 —– 他们大致上是基于,我作为一个专业软体工程师的广泛生涯,从 Clojure 社群成员所得到的反馈及建议,和数个高度评价的 Clojure 编程资源,像是?"Clojure Programming"以及?"The Joy of Clojure"。
本指南仍在完善中 –– 缺少某些章节,某些不完整,某些规则缺少例子,某些规则例子演示不够清楚。在完稿时,将会解决这些议题 –– 现在就先记在心上就好。
你可以使用?Transmuter?来产生本指南的一份 PDF 或 HTML 复本。
几乎每人都深信,每一个除了自己的风格都又丑又难读。
把 "除了自己的" 拿掉,他们或许是对的...
-- Jerry Coffin (论缩排)
每个缩排层级使用两个空格。不要使用 Hard Tabs。
;; good(when something (something-else));; bad - 四个空格(when something (something-else))
垂直排列函数参数。
;; good(filter even? (range 1 10));; bad(filter even? (range 1 10))
排列?let
?的绑定与 map 的关键字。
;; 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
,选择性忽略函数名与参数向量之间的新行。
;; good(defn foo [x] (bar x));; good(defn foo [x] (bar x));; bad(defn foo [x] (bar x))
选择性忽略短的参数向量与函数体之间的新行。
;; good(defn foo [x] (bar x));; 短的函数这样写很好(defn goo [x] (bar x));; 多参数的函数这样写很好(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 "Hello there. This is a multi-line docstring." [] (bar));; bad(defn foo "Hello there. This isa multi-line docstring." [] (bar))
使用 Unix 风格的行编码(BSD/Solaris/Linux/OSX 的用户不用担心,,Windows 用户要格外小心。)
如果你使用 Git ,你也许会想加入下面这个配置,来保护你的项目被 Windows 的行编码侵入:
$ git config --global core.autocrlf true
若有任何文字在左括号、中括号、大括号前((
,?[
,?{
),或是在右括号、中括号、大括号之后()
,?]
,?}
),将文字与括号用一个空格分开。反过来说,在左括号后、右括号前不要有空格。
;; good(foo (bar baz) quux);; bad(foo(bar baz)quux)(foo ( bar baz ) quux)
不要在循序的复合类型的字面常量语法里使用逗号。
;; good[1 2 3](1 2 3);; bad[1, 2, 3](1, 2, 3)
明智的使用逗号与断行来加强 map 的可读性。
;; 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(when something (something-else));; bad(when something (something-else))
顶层形式用空行间隔开来。
;; good(def x ...)(defn foo ...);; bad(def x ...)(defn foo ...)
函数或宏定义中间不要放空行。
每个命名空间用?ns
?形式开始,加上?refer
、require
、use
?以及?import
。
(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)))
避免单段的命名空间。
;; good(ns example.ns);; bad(ns example)
避免使用过长的命名空间(也就是超过 5 段)
函数避免超过 10 行代码。多数函数应小于 5 行。
参数列表避免超过 3 个或 4 个位置参数(positional parameters)。
require
?与?refer
。他们在 REPL 之外完全用不到。declare
?来启用 forward references。偏好像是?map
?与?loop/recur
?的高阶函数。
函数体内偏好使用 pre 函数与 post 条件来检查。
;; 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!")))
不要在函数内定义变量。
;; very bad(defn foo [] (def x 5) ...)
不要用局域绑定遮蔽?clojure.core
?内的名称。
;; bad - you're forced to used clojure.core/map fully qualified inside(defn foo [map] ...)
使用?seq
?作为终止条件来测试序列是否为空(这个技巧有时候称为?nil punning)。
;; 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))))
用?when
?取代?(if ... (do ...)
。
;; good(when pred (foo) (bar));; bad(if pred (do (foo) (bar)))
用?if-let
?取代?let
?+?if
。
;; good(if-let [result :foo] (something-with result) (something-else));; bad(let [result :foo] (if result (something-with result) (something-else)))
用?when-let
?取代?let
?+?when
。
;; good(when-let [result :foo] (do-something-with result) (do-something-more-with result));; bad(let [result :foo] (when result (do-something-with result) (do-something-more-with result)))
用?if-not
?取代?(if (not ...) ...)
。
;; good(if-not (pred) (foo));; bad(if (not pred) (foo))
用?when-not
?取代?(when (not ...) ...)
。
;; good(when-not pred (foo) (bar));; bad(when (not pred) (foo) (bar))
用?not=
?取代?(not (= ...))
。
;; good(not= foo bar);; bad(not (= foo bar))
偏好?%
?胜于?%1
?在只有一个参数的函数字面常量。
;; good#(Math/round %);; bad#(Math/round %1)
偏好?%1
?胜于?%
?在超过一个参数的函数字面常量。
;; good#(Math/pow %1 %2);; bad#(Math/pow % %2)
不要在不必要的情况用匿名函数包装函数。
;; good(filter even? (range 1 10));; bad(filter #(even? %) (range 1 10))
若函数体由一个以上形式组成,不要使用函数的字面常量语法。
;; good(fn [x] (println x) (* x 2));; bad (you need an explicit do form)#(do (println %) (* % 2))
complement
?与使用匿名函数相比,喜好使用前者。
;; good(filter (complement some-pred?) coll);; bad(filter #(not (some-pred? %)) coll)
这个规则应该在函数有明确的反函数时忽略(如:even?
?与?odd?
)。
在可以产生更简洁代码的情况时利用?comp
。
;; good(map #(capitalize (trim %)) ["top " " test "]);; better(map (comp capitalize trim) ["top " " test "])
在可以产生更简洁代码的情况时利用?partial
?。
;; good(map #(+ 5 %) (range 1 10));; (arguably) better(map (partial + 5) (range 1 10))
偏好使用 threading macros?->
?(thread-first)及?->>
?(thread-last)来简化嵌套形式。
;; 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)))
当连锁调用 Java interop 的方法时,偏好?..
?胜于?->
。
;; good(-> (System/getProperties) (.get "os.name"));; better(.. System getProperties (get "os.name"))
在?cond
?与?condp
?使用?:else
?作为最后的测试表达式。
;; good(cond (< n 0) "negative" (> n 0) "positive" :else "zero"));; bad(cond (< n 0) "negative" (> n 0) "positive" true "zero"))
当谓词与表达式不变时,偏好用?condp
?来取代?cond
。
;; good(cond (= x 10) :ten (= x 20) :twenty (= x 30) :forty :else :dunno);; much better(condp = x 10 :ten 20 :twenty 30 :forty :dunno)
当测试表达式是编译期时间常量时,偏好使用?case
?取代?cond
?或?condp
。
;; 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)
适当的时机下使用?set
?作为谓词。
;; bad(remove #(= % 0) [0 1 2 3 4 5]);; good(remove #{0} [0 1 2 3 4 5]);; bad(count (filter #(or (= % \a) (= % \e) (= % \i) (= % \o) (= % \u)) "mary had a little lamb"));; good(count (filter #{\a \e \i \o \u} "mary had a little lamb"))
使用?(inc x)
?&?(dec x)
?而不是?(+ x 1)
?and?(- x 1)
。
使用?(pos? x)
,?(neg? x)
?&?(zero? x)
?而不是?(> x 0)
,?(< x 0)
?&?(= x 0)
。
使用包装好的 Java interop 形式。
;;; 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;; goodInteger/MAX_VALUE;; bad(. Integer MAX_VALUE);;; instance field access;; good(.someField some-object);; bad(. some-object some-field)
程式设计的真正难题是替事物命名及无效的缓存。?
-- Phil Karlton
project.module
organization.project.module
lisp-case
?。lisp-case
。even?
)。reset!
)。conversation 函数使用?->
?取代?to
。
;; good(defn f->c ...);; not so good(defn f-to-c ...)
使用?*earmuffs*
?耳套(星号)给将会重新绑定的东西(也就是动态的)。
_
?for destructuring targets and formal arguments names whose value will be ignored by the code at hand.clojure.core
?的范例,如?pred
?与?coll
。f
,?g
,?h
?- 函数输入n
?- 整数输入(通常是大小)index
?- 整数索引x
,?y
?- 数字s
?- 字串输入coll
?- 复合类型pred
?- 谓词闭包& more
?- 可变输入expr
?- 表达式body
?- 宏的主体binding
?- 宏的绑定向量单数据结构与百个函数,好过十个函数与数据结构?
-- Alan J. Perlis
哈希键偏好使用关键字。
;; good{:name "Bruce" :age 30};; bad{"name" "Bruce" "age" 30}
在允许的场合下,偏好使用复合类型的字面常量语法。但在定义集合时,当数值为编译期时间常量时,仅使用字面常量语法。
;; 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)
在任何情况下避免通过索引来访问复合类型的成员。
在允许的场合下,偏好使用作为关键字的函数来从 map 取出数值。
(def m {:name "Bruce" :age 30});; good(:name m);; bad - 太罗嗦(get m :name);; bad - 有 NullPointerException 之虞(m :name)
利用多数复合类型是其元素的函数这个事实。
;; good(filter #{\a \e \o \i \u} "this is a test");; 差劲 - 烂到不敢给你看
利用关键字可以当作复合类型的函数这个事实。
((juxt :a :b) {:a "ala" :b "bala"})
避免使用过渡的复合类型,除非在攸关性能的部分代码使用。
避免使用 Java 的 collections。
除了 interop 与攸关性能的代码(大量处理原生类型的代码)外,避免使用 Java 的数组。
io!
?宏的 IO 调用包起来,来避免在事务中不小心调用到这些代码。ref-set
?。send
?。send-off
?。reset!
?。偏好使用?clojure.string
?里定义的字串操作函数,而不是 Java interop,或是自己写。
;; good(clojure.string/upper-case "bruce");; bad(.toUpperCase "bruce")
java.lang.IllegalArgumentException
、java.lang.UnsupportedOperationException
、java.lang.IllegalStateException
、java.io.IOException
)。with-open
?胜于?finally
。良好的代码是最佳的文档。当你要加一个注释时,扪心自问,
"如何改善代码让它不需要注释?" 改善代码然后记录下来使它更简洁。?
-- 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
持续更新注解。过时的注解比没有注解还糟糕。
当你需要注解一个特定的形式时,偏好使用?#_
?读取宏胜于一般的注解。
;; 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
?来标记之后应被加入的未实现功能或特色。FIXME
?来标记一个需要修复的代码。OPTIMIZE
?来标记可能影响性能的缓慢或效率低落的代码。HACK
?来标记代码异味,其中包含了可疑的编码实践以及应该需要重构。REVIEW
?来标记任何需要审视及确认正常动作的地方。举例来说:?REVIEW: 我们确定用户现在是这么做的吗?
README
?或类似的地方。在本指南所写的每个东西都不是定案。这只是我渴望想与同样对 Clojure 编程风格有兴趣的大家一起工作,以致于最终我们可以替整个 Clojure 社群创造一个有益的资源。
欢迎开票或发送一个带有改进的更新请求。在此提前感谢你的帮助!
一份社群策动的风格指南,对一个社群来说,只是让人知道有这个社群。微博转发这份指南,分享给你的朋友或同事。我们得到的每个注解、建议或意见都可以让这份指南变得更好一点。而我们想要拥有的是最好的指南,不是吗?
?
?
?
?