Clojure 风格指南

原文:Clojure Style Guide 譯者:JuanitoFatas http://en.wikipedia.org/

Clojure 风格指南

这篇 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 is
    a 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 ...)
    
  • 函数或宏定义中间不要放空行。

  • 可行的场合下,避免每行超过 80 字符。
  • 避免尾随的空白。
  • 一个文件、一个命名空间。
  • 每个命名空间用 ns 形式开始,加上 referrequireuse 以及 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)。

语法

  • 避免使用操作命名空间的函数,像是:requirerefer。他们在 REPL 之外完全用不到。
  • 使用 declare 来启用 forward references。
  • 偏好像是 maploop/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"))
    
  • condcondp 使用 :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 取代 condcondp

    ;; 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
    ;; good
    Integer/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
  • 协议、记录、结构及类型使用驼峰形式(专有缩略词保持大写:HTTP、RFC、XML)
  • 谓词方法的名字(返回布尔值的方法)以问号结尾(例:even?)。
  • STM 事务里不安全的函数、宏的名字以惊叹号结尾(例:reset!)。
  • conversation 函数使用 -> 取代 to

    ;; good
    (defn f->c ...)
    
    ;; not so good
    (defn f-to-c ...)
    
  • 使用 *earmuffs* 耳套(星号)给将会重新绑定的东西(也就是动态的)。

  • 常量不要使用特殊的表示法;除非特别说明,假设一切都是常量。
  • Use _ for destructuring targets and formal arguments names whose value will be ignored by the code at hand.
  • 惯用名遵循 clojure.core 的范例,如 predcoll
    • 函数:
      • 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 的数组。

Mutation

Refs

  • 考虑看看将所有带有 io! 宏的 IO 调用包起来,来避免在事务中不小心调用到这些代码。
  • 无论何时都避免使用 ref-set
  • 试著使事务的大小(封装在事务里的工作量)越小越好。
  • 避免有短期、长期与同一个 Ref 互动的事务。

Agents

  • 仅针对 CPU 绑定或不阻塞 IO、其他线程的动作使用 send
  • 给看起来可能会阻塞、睡眠或阻碍线程的动作使用 send-off

原子

  • 避免在 STM 事务里更新原子。
  • 无论何时都避免使用 reset!

字串

  • 偏好使用 clojure.string 里定义的字串操作函数,而不是 Java interop,或是自己写。

    ;; good
    (clojure.string/upper-case "bruce")
    
    ;; bad
    (.toUpperCase "bruce")
    

异常

  • 重用现有的异常类型。符合语言习惯的 Clojure 代码,当真的抛出异常时,会抛出标准类型的异常(如 java.lang.IllegalArgumentExceptionjava.lang.UnsupportedOperationExceptionjava.lang.IllegalStateExceptionjava.io.IOException)。
  • 偏好使用 with-open 胜于 finally

  • 不要在函数可以办到的情况下使用宏。
  • 先撰写宏的用途的示例子,再开始撰写宏。
  • 不管是什么时候,只要可能的话,将复杂的宏拆成较小的函数。
  • 宏应该仅作为提供语法糖的功能,其核心为清晰的函数。这么做会改善可组合性 (composability)。
  • 偏好引用形式语法胜于手动构造列表

注解

良好的代码是最佳的文档。当你要加一个注释时,扪心自问,
"如何改善代码让它不需要注释?" 改善代码然后记录下来使它更简洁。
-- 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

  • 避免撰写注解来解释糟糕的代码。重构代码使其一目了然 (要嘛就做,要嘛不做 –― 不要只是试试看。–– Yoda)

注释

  • 注释通常会直接写在相关代码的那行后面。
  • 注释关键字后面接著一个冒号与空格,接著是描述问题的说明。
  • 如果描述问题需要多行时,之后的行需与第一行对齐。
  • 将注释打上名字缩写与日期标签,这样之后才可轻松识别出来。

    (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 或类似的地方。

基本原则

  • 用函数式风格来编程。适当的避免 mutation。
  • 保持一致。在理想的世界里,与这些准则保持一致。
  • 使用常识。

贡献

在本指南所写的每个东西都不是定案。这只是我渴望想与同样对 Clojure 编程风格有兴趣的大家一起工作,以致于最终我们可以替整个 Clojure 社群创造一个有益的资源。

欢迎开票或发送一个带有改进的更新请求。在此提前感谢你的帮助!

口耳相传

一份社群策动的风格指南,对一个社群来说,只是让人知道有这个社群。微博转发这份指南,分享给你的朋友或同事。我们得到的每个注解、建议或意见都可以让这份指南变得更好一点。而我们想要拥有的是最好的指南,不是吗?