Skip to content
📖 一份由社区驱动的Clojure 编程代码规范
Branch: master
Clone or download
Pull request Compare This branch is 3 commits ahead, 43 commits behind bbatsov:master.
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
CONTRIBUTING.md
README.md

README.md

Clojure 代码规范

Role models are important.
-- Officer Alex J. Murphy / RoboCop

原文地址:https://github.com/bbatsov/clojure-style-guide

这份Clojure代码规范旨在提供一系列的最佳实践,让现实工作中的Clojure程序员能够写出易于维护的代码,并能与他人协作和共享。一份反应真实需求的代码规范才能被人接收,而那些理想化的、甚至部分观点遭到程序员拒绝的代码规范注定不会长久——无论它有多出色。

这份规范由多个章节组成,每个章节包含一组相关的规则。我会尝试去描述每条规则背后的理念(过于明显的理念我就省略了)。

这些规则并不是我凭空想象的,它们出自于我作为一个专业软件开发工程师长久以来的工作积累,以及Clojure社区成员们的反馈和建议,还有各种广为流传的Clojure编程学习资源,如《Clojure Programming》、《The Joy of Clojure》等。

这份规范还处于编写阶段,部分章节有所缺失,内容并不完整;部分规则没有示例,或者示例还不能完全将其描述清楚。未来这些问题都会得到改进,只是请你了解这一情况。

你可以使用Transmuter生成一份本规范的PDF或HTML格式的文档。

本指南的翻译可在以下几种语言中:

目录

源代码的布局和组织结构

几乎所有人都认为任何代码风格都是丑陋且难以阅读的,除了自己的之外。 把这句话中的“除了自己之外”去掉,那差不多就能成立了。
—— Jerry Coffin 关于代码缩进的评论

  • 使用两个空格进行缩进,不使用制表符。 [链接]

  • 使用2个空格来缩进含参数部分的形式, 。这些形式包括所有的 def 形式 ,特实形式和宏, 以及本地绑定形式 (例如: loop, let, when-let) 和许多像 when, cond, as->, cond->, case, with-*等的宏。 [链接]

    ;; 很好
    (when something
      (something-else))
    
    (with-out-str
      (println "Hello, ")
      (println "world!"))
    
    ;; 糟糕 - 四个空格
    (when something
        (something-else))
    
    ;; 糟糕 - 一个空格
    (with-out-str
     (println "Hello, ")
     (println "world!"))
  • 垂直排列函数参数。 [链接]

    ;; 很好
    (filter even?
            (range 1 10))
    
    ;; 糟糕
    (filter even?
      (range 1 10))
  • 使用一个空格缩进函数(宏)参数 当函数没有参数独占一行。 [链接]

    ;; 很好
    (filter
     even?
     (range 1 10))
    
    (or
     ala
     bala
     portokala)
    
    ;; 糟糕 - 两个空格缩进
    (filter
      even?
      (range 1 10))
    
    (or
      ala
      bala
      portokala)
  • 对齐let绑定,以及map类型中的关键字。 [链接]

    ;; 很好
    (let [thing1 "some stuff"
          thing2 "other stuff"]
      {:thing1 thing1
       :thing2 thing2})
    
    ;; 糟糕
    (let [thing1 "some stuff"
      thing2 "other stuff"]
      {:thing1 thing1
      :thing2 thing2})
  • 针对没有文档字串的 defn,选择性忽略函数名与参数向量之间的新行。 [链接]

    ;; 很好
    (defn foo
      [x]
      (bar x))
    
    ;; 很好
    (defn foo [x]
      (bar x))
    
    ;; 糟糕
    (defn foo
      [x] (bar x))
  • 将一个多重方法的dispatch-val 与函数名放置在同一行。 [链接]

    ;; 很好
    (defmethod foo :bar [x] (baz x))
    
    (defmethod foo :bar
      [x]
      (baz x))
    
    ;; 糟糕
    (defmethod foo
      :bar
      [x]
      (baz x))
    
    (defmethod foo
      :bar [x]
      (baz x))
  • 当为采用上述形式的函数添加字符串文档 - 注意正确应放置到函数名后面,而不是参数列表 后面。后者虽然不是无效语法,不会造成错误, 但是这仅仅只是将字符串作为函数体的一种形式,而不将其链接为 该变量的文档。 [链接]

      ;; 很好
      (defn foo
        "docstring"
        [x]
        (bar x))
    
      ;; 糟糕
      (defn foo [x]
        "docstring"
        (bar x))
  • 选择性忽略短的参数向量与函数体之间的新行。 [链接]

    ;; 很好
    (defn foo [x]
      (bar x))
    
    ;; 适合简单的函数
    (defn foo [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)))
  • 多元函数定义,各元数形式垂直对齐参数。 [链接]

    ;; 很好
    (defn foo
      "I have two arities."
      ([x]
       (foo x 1))
      ([x y]
       (+ x y)))
    
    ;; 糟糕 - 多出的缩进
    (defn foo
      "I have two arities."
      ([x]
        (foo x 1))
      ([x y]
        (+ x y)))
  • 按照从少到多的参数,排序函数的多元数形式。多元素的情况下,共同 功能是某个k参数完全指定函数的 行为,并且元素个数 Ñ < K 部分地应用在K元数,和 元素 N> K提供在K元数超过可变参数的实现。 [链接]

    ;; 很好 - 很容易扫描第n个参数形式
    (defn foo
      "I have two arities."
      ([x]
       (foo x 1))
      ([x y]
       (+ x y)))
    
    ;; 还好 - 其他元素应用两倍元数形式
    (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)))
    
    ;; 糟糕 - 无序的,毫无理由这样
    (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)))
  • 缩进多行的文档字串。 [链接]

    ;; 很好
    (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 的行编码侵入:
    bash$ git config --global core.autocrlf true
    
  • 若有任何文字在左括号、中括号、大括号前((, [, {),或是在右括号、中括号、大括号之后(), ], }),将文字与括号用一个空格分开。反过来说,在左括号后、右括号前不要有空格。 [链接]

    ;; 很好
    (foo (bar baz) quux)
    
    ;; 糟糕
    (foo(bar baz)quux)
    (foo ( bar baz ) quux)

Syntactic sugar causes semicolon cancer.
-- Alan Perlis

  • 不要在序列化的集合类型的字面常量语法里使用逗号。 [链接]

    ;; 很好
    [1 2 3]
    (1 2 3)
    
    ;; 糟糕
    [1, 2, 3]
    (1, 2, 3)
  • 明智的使用逗号与换行来加强 map 的可读性。 [链接]

    ;; 很好
    {:name "Bruce Wayne" :alter-ego "Batman"}
    
    ;; 很好, 且会增强可读性
    {:name "Bruce Wayne"
     :alter-ego "Batman"}
    
    ;; 很好, 且较为紧凑
    {:name "Bruce Wayne", :alter-ego "Batman"}
  • 将所有尾括号放在同一行。 [链接]

    ;; 很好; 同一行
    (when something
      (something-else))
    
    ;; 糟糕; 不同行
    (when something
      (something-else)
    )
  • 顶层形式用空行间隔开来。 [链接]

    ;; 很好
    (def x ...)
    
    (defn foo ...)
    
    ;; 糟糕
    (def x ...)
    (defn foo ...)

    一个例外是相关def分组在一起。

    ;; 很好
    (def min-rows 10)
    (def max-rows 20)
    (def min-cols 15)
    (def max-cols 30)
  • 函数或宏定义中间不要放空行。一个例外,可制成以指示分组 比如发现成对结构 let cond[链接]

  • 可行的场合下,避免每行超过 80 字符。 [链接]

  • 避免尾随的空白。 [链接]

  • 一个文件、一个命名空间。 [链接]

  • 每个命名空间用 ns 形式开始,加上 referrequireuse 以及 import[链接]

    (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
                                     链接edBlockingQueue]))
  • ns 宏中优先使用 :require :as 胜于 :require :refer 胜于 :require :refer :all. 优先使用 :require 胜于 :use; 后者的形式应该是 考虑使用新的代码。 [链接]

    ;; 很好
    (ns examples.ns
      (:require [clojure.zip :as zip]))
    
    ;; 很好
    (ns examples.ns
      (:require [clojure.zip :refer [lefts rights]))
    
    ;; 可以接受的
    (ns examples.ns
      (:require [clojure.zip :refer :all]))
    
    ;; 糟糕
    (ns examples.ns
      (:use clojure.zip))
  • 避免单段的命名空间。 [链接]

    ;; 很好
    (ns example.ns)
    
    ;; 糟糕
    (ns example)
  • 避免使用过长的命名空间(不超过五段)。 [链接]

  • 函数避免超过 10 行代码。事实上,大多数函数应保持在5行代码以内。 [链接]

  • 参数列表避免超过 3 个或 4 个位置参数。 [链接]

  • 避免向前引用。它们偶尔必要的,但这样的场合 实际上很罕见。 [链接]

语法

  • 避免使用操作命名空间的函数,像是:requirerefer。他们在 REPL 之外完全用不到。 [链接]

  • 使用declare实现向前引用。 [链接]

  • 优先使用map这类高阶函数,而非loop/recur[链接]

  • 优先使用前置、后置条件来检测函数参数和返回值。 [链接]

    ;; 很好
    (defn foo [x]
      {:pre [(pos? x)]}
      (bar x))
    
    ;; 糟糕
    (defn foo [x]
      (if (pos? x)
        (bar x)
        (throw (IllegalArgumentException. "x must be a positive number!")))
  • 不要在函数中定义变量。 [链接]

    ;; 非常糟糕
    (defn foo []
      (def x 5)
      ...)
  • 本地变量名不应覆盖clojure.core中定义的函数。 [链接]

    ;; 糟糕 - 这样一来函数中调用`clojure.core/map`时就需要指定完整的命名空间了。
    (defn foo [map]
      ...)
  • 使用 alter-var-root 替代 def 去改变变量的值。

    ;; 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)。 [链接]

    ;; 很好
    (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))))
  • 需要将序列转换向量,优先使用 vec 而不是 into[链接]

    ;; 很好
    (vec some-seq)
    
    ;; 糟糕
    (into [] some-seq)
  • 使用 when 替代 (if ... (do ...)[链接]

    ;; 很好
    (when pred
      (foo)
      (bar))
    
    ;; 糟糕
    (if pred
      (do
        (foo)
        (bar)))
  • 使用 if-let 替代 let + if[链接]

    ;; 很好
    (if-let [result (foo x)]
      (something-with result)
      (something-else))
    
    ;; 糟糕
    (let [result (foo x)]
      (if result
        (something-with result)
        (something-else)))
  • 使用 when-let 替代 let + when[链接]

    ;; 很好
    (when-let [result (foo x)]
      (do-something-with result)
      (do-something-more-with result))
    
    ;; 糟糕
    (let [result (foo x)]
      (when result
        (do-something-with result)
        (do-something-more-with result)))
  • 使用 if-not 替代 (if (not ...) ...)[链接]

    ;; 很好
    (if-not pred
      (foo))
    
    ;; 糟糕
    (if (not pred)
      (foo))
  • 使用 when-not 替代 (when (not ...) ...)[链接]

    ;; 很好
    (when-not pred
      (foo)
      (bar))
    
    ;; 糟糕
    (when (not pred)
      (foo)
      (bar))
  • 使用 when-not 替代 (if-not ... (do ...)[链接]

    ;; 很好
    (when-not pred
      (foo)
      (bar))
    
    ;; 糟糕
    (if-not pred
      (do
        (foo)
        (bar)))
  • 使用 not= 替代 (not (= ...))[链接]

    ;; 很好
    (not= foo bar)
    
    ;; 糟糕
    (not (= foo bar))
  • 使用 printf 替代 (print (format ...))[链接]

    ;; 很好
    (printf "Hello, %s!\n" name)
    
    ;; 好
    (println (format "Hello, %s!" name))
  • 在做比较,请考虑, Clojure的函数< >等,接受可变数量的参数的函数。 [链接]

    ;; 很好
    (< 5 x 10)
    
    ;; 糟糕
    (and (> x 5) (< x 10))
  • 当匿名函数只有一个参数时,优先使用 % ,而非 %1[链接]

    ;; 很好
    #(Math/round %)
    
    ;; 糟糕
    #(Math/round %1)
  • 当匿名函数有多个参数时,优先使用 %1,而非 %[链接]

    ;; 很好
    #(Math/pow %1 %2)
    
    ;; 糟糕
    #(Math/pow % %2)
  • 只有在必要的时候才使用匿名函数。 [链接]

    ;; 很好
    (filter even? (range 1 10))
    
    ;; 糟糕
    (filter #(even? %) (range 1 10))
  • 若函数体由一个以上形式组成,不要使用匿名函数。 [链接]

    ;; 很好
    (fn [x]
      (println x)
      (* x 2))
    
    ;; 糟糕 (你需要明确得使用到do)
    #(do (println %)
         (* % 2))
  • 在特定情况下优先使用complement,而非匿名函数。 [链接]

    ;; 很好
    (filter (complement some-pred?) coll)
    
    ;; 糟糕
    (filter #(not (some-pred? %)) coll)

    这个规则应该在函数有明确的反函数时忽略(如:even? 与 odd?)。

  • 某些情况下可以用 comp 使代码更简洁。 [链接]

    ;; Assuming `(:require [clojure.string :as str])`...
    
    ;; 很好
    (map #(str/capitalize (str/trim %)) ["top " " test "])
    
    ;; 更好
    (map (comp str/capitalize str/trim) ["top " " test "])
  • 某些情况下可以用 partial 使代码更简洁。 [链接]

    ;; 很好
    (map #(+ 5 %) (range 1 10))
    
    ;; (或许) 更好
    (map (partial + 5) (range 1 10))
  • 当遇到嵌套调用时,建议使用 -> 宏和 ->> 宏。 [链接]

    ;; 很好
    (-> [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类的方法时,优先使用 .. ,而非 ->[链接]

    ;; 很好
    (-> (System/getProperties) (.get "os.name"))
    
    ;; 更好
    (.. System getProperties (get "os.name"))
  • condcondp 中,使用 :else 来处理不满足条件的情况。 [链接]

    ;; 很好
    (cond
      (< n 0) "negative"
      (> n 0) "positive"
      :else "zero"))
    
    ;; 糟糕
    (cond
      (< n 0) "negative"
      (> n 0) "positive"
      true "zero"))
  • 当比较的变量和方式相同时,优先使用 condp ,而非 cond[链接]

    ;; 很好
    (cond
      (= x 10) :ten
      (= x 20) :twenty
      (= x 30) :forty
      :else :dunno)
    
    ;; 更好
    (condp = x
      10 :ten
      20 :twenty
      30 :forty
      :dunno)
  • 当条件是常量时,优先使用 case ,而非 condcondp[链接]

    ;; 很好
    (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)
  • 如果不能在视觉上使用注释与空行两两分组提示,则在 cond 相关的的形式中,使用短形式。 [链接]

    ;; 很好
    (cond
      (test1) (action1)
      (test2) (action2)
      :else   (default-action))
    
    ;; 还行
    (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 作为判断条件。 [链接]

    ;; 很好
    (remove #{1} [0 1 2 3 4 5])
    
    ;; 糟糕
    (remove #(= % 1) [0 1 2 3 4 5])
    
    ;; 很好
    (count (filter #{\a \e \i \o \u} "mary had a little lamb"))
    
    ;; 糟糕
    (count (filter #(or (= % \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)[链接]

  • Useinstead of a series of nestedinvocations. 使用 list* 替代内部嵌套多个 cons[链接]

    # 很好
    (list* 1 2 3 [4 5])
    
    # 糟糕
    (cons 1 (cons 2 (cons 3 [4 5])))
    
  • 进行Java交互时,优先使用Clojure提供的语法糖。 [链接]

    ;;; 创建对象
    ;; 很好
    (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 someField)
  • Use the compact metadata notation for metadata that contains only slots whose keys are keywords and whose value is boolean . 当元数据的键是关键字和值是 true ,使用紧凑形式标记元数据。 [链接]

    ;; 很好
    (def ^:private a 5)
    
    ;; 糟糕
    (def ^{:private true} a 5)
  • 指出代码的私有部分。 [链接]

    ;; 很好
    (defn- private-fun [] ...)
    
    (def ^:private private-var ...)
    
    ;; 糟糕
    (defn private-fun [] ...) ; not private at all
    
    (defn ^:private private-fun [] ...) ; overly verbose
    
    (def private-var ...) ; not private at all
  • 使用 @#'some.ns/var 形式,访问私有变量(如:为了测试)。 [链接]

  • Be careful regarding what exactly do you attach metadata to. 小心你添加元数据的对象。 [链接]

    ;; 我们添加元数据到变量 `a`中
    (def ^:private a {})
    (meta a) ;=> nil
    (meta #'a) ;=> {:private true}
    
    ;; 我们添加元数据到空的hash-map中
    (def a ^:private {})
    (meta a) ;=> {:private true}
    (meta #'a) ;=> nil

命名

编程中真正的难点只有两个:验证缓存的有效性;命名。 -- Phil Karlton

  • 命名空间建议使用以下两种方式: [链接]

    • 项目名称.模块名称
    • 组织名称.项目名称.模块名称
  • 对于命名空间中较长的元素,使用 lisp-case 格式,如(bruce.project-euler)。 [链接]

  • 使用 lisp-case 格式来命名函数和变量。 [链接]

    ;; 很好
    (def some-var ...)
    (defn some-fun ...)
    
    ;; 糟糕
    (def someVar ...)
    (defn somefun ...)
    (def some_fun ...)
  • 使用 CamelCase 来命名接口(protocol)、记录(record)、结构和类型(struct & type)。对于HTTP、RFC、XML等缩写,仍保留其大写格式。 [链接]

  • 对于返回布尔值的函数名称,使用问号结尾,(如: even?)。 [链接]

     ;; 很好
     (defn palindrome? ...)
    
     ;; 糟糕
     (defn palindrome-p ...) ; Common Lisp style
     (defn is-palindrome ...) ; Java style
  • The names of functions/macros that are not safe in STM transactions should end with an exclamation mark (e.g. reset!). 当方法或宏不能在STM中安全使用时,须以感叹号结尾,(如:reset!)。 [链接]

  • 命名类型转换函数时使用 -> ,而非 to[链接]

    ;; 很好
    (defn f->c ...)
    
    ;; 不够好
    (defn f-to-c ...)
  • 对于可供重绑定的变量(即动态变量),使用星号括起,(如:earmuffs)。 [链接]

    ;; good
    (def ^:dynamic *a* 10)
    
    ;; bad
    (def ^:dynamic a 10)
  • 无需对常量名进行特殊的标识,因为所有的变量都应该是常量,除非有特别说明。 [链接]

  • 对于解构过程中或参数列表中忽略的元素,使用 _ 来表示。 [链接]

      ;; 很好
      (let [[a b _ c] [1 2 3 4]]
        (println a b c))
    
      (dotimes [_ 3]
        (println "Hello!"))
    
      ;; 糟糕
      (let [[a b c d] [1 2 3 4]]
        (println a b d))
    
      (dotimes [i 3]
        (println "Hello!"))
  • 参考 clojure.core 中的命名规范,如 predcoll[链接]

    • 函数:
      • fgh - 参数内容是一个函数
      • n - 整数,通常是一个表示大小的值
      • index, i - 整数索引
      • x, y - 数值
      • xs - 序列
      • m - 映射
      • s - 字符串
      • re - 正则表达式
      • coll - 集合
      • pred - 谓词闭包
      • & more - 可变参数
      • xf - xform, 一个转换器
    • 宏:
      • expr - 表达式
      • body - 宏的主体
      • binding - 一个向量,包含宏的绑定

集合

用100种函数去操作同一种数据结构,要好过用10种函数操作10种数据结构。
-- Alan J. Perlis

  • 避免使用列表(list)来存储数据(除非它真的就是你想要的)。 [链接]

  • 优先使用关键字(keyword),而非普通的哈希键。 [链接]

    ;; 很好
    {:name "Bruce" :age 30}
    
    ;; 糟糕
    {"name" "Bruce" "age" 30}
  • 编写集合时,优先使用字面的语法形式,而非构造函数。但是,在定义唯一值集合(set)时,只有当元素都是常量时才可使用字面语法,否则应使用构造函数。 [链接]

     ;; 很好
     [1 2 3]
     #{1 2 3}
     (hash-set (func1) (func2)) ; 元素在运行时确定
    
     ;; 糟糕
     (vector 1 2 3)
     (hash-set 1 2 3)
     #{(func1) (func2)} ; 若(func1)和(func2)的值相等,则会抛出运行时异常。
  • 避免使用数值索引来访问集合元素。 [链接]

  • 优先使用关键字来获取哈希表(map)中的值。 [链接]

    (def m {:name "Bruce" :age 30})
    
    ;; 很好
    (:name m)
    
    ;; 太过啰嗦
    (get m :name)
    
    ;; 糟糕 - 可能抛出空指针异常
    (m :name)
  • 集合可以被用作函数。 [链接]

    ;; 很好
    (filter #{\a \e \o \i \u} "this is a test")
    
    ;; 糟糕 - 不够美观
  • 关键字可以被用作函数。 [链接]

    ((juxt :a :b) {:a "ala" :b "bala"})
  • 只有在非常强调性能的情况下才可使用瞬时集合(transient collection)。 [链接]

  • 避免使用Java集合。 [链接]

  • 避免使用Java数组,除非遇到需要和Java类进行交互,或需要高性能地处理基本类型时才可使用。 [链接]

可变

引用(Refs)

  • 建议所有的IO操作都使用 io! 宏进行包装,以免不小心在事务中调用了这些代码。 [链接]

  • 避免使用 ref-set[链接]

    (def r (ref 0))
    
    ;; 很好
    (dosync (alter r + 5))
    
    ;; 糟糕
    (dosync (ref-set r 5))
  • 控制事务的大小,(即事务所执行的工作越少越好)。 as small as possible. [链接]

  • 避免出现短期事务和长期事务访问同一个引用(Ref)的情形。 [链接]

代理(Agents)

  • send 仅使用于计算密集型、不会因IO等因素阻塞的线程。 [链接]

  • send-off 则用于会阻塞、休眠的线程。 [链接]

原子(Atoms)

  • 避免在事务中更新原子。 [链接]

  • 尽量使用 swap! ,而不是 reset![链接]

    (def a (atom 0))
    
    ;; 很好
    (swap! a + 5)
    
    ;; 不够好
    (reset! a 5)

字符串

  • 优先使用 clojure.string 中提供的字符串操作函数,而不是Java中提供的或是自己编写的函数。 [链接]

    ;; 很好
    (clojure.string/upper-case "bruce")
    
    ;; 糟糕
    (.toUpperCase "bruce")

异常

  • 复用已有的异常类型,符合语言习惯的 Clojure 代码,当真的抛出异常时,会抛出标准类型的异常 (如: java.lang.IllegalArgumentException, java.lang.UnsupportedOperationException, java.lang.IllegalStateException, java.io.IOException). [链接]

  • 优先使用 with-open ,而非 finally[链接]

  • 如果可以用函数实现相同功能,不要编写一个宏。 [链接]

  • 首先编写一个宏的用例,尔后再编写宏本身。 [链接]

  • 尽可能将一个复杂的宏拆解为多个小型的函数。 [链接]

  • 宏只应用于简化语法,其核心应该是一个普通的函数。 [链接]

  • 使用语法转义(syntax-quote,即反引号),而非手动构造list。 [链接]

注释

好的代码本身就是文档。因此在添加注释之前,先想想自己该如何改进代码,让它更容易理解。做到这一点后,再通过注释让代码更清晰。
-- Steve McConnell

  • 学会编写容易理解的代码,然后忽略下文的内容。真的! [链接]

  • 对于标题型的注释,使用至少四个分号起始。 [链接]

  • 对于顶层注释,使用三个分号起始。 [链接]

  • 为某段代码添加注释时,使用两个分号起始,且应与该段代码对齐。 [链接]

  • 对于行尾注释,使用一个分号起始即可。 [链接]

  • 分号后面要有一个空格。 [链接]

    ;;;; Frob Grovel
    
    ;;; 这段代码有以下前提:
    ;;;   1. Foo.
    ;;;   2. Bar.
    ;;;   3. Baz.
    
    (defn fnord [zarquon]
      ;; If zob, then veeblefitz.
      (quux zot
            mumble             ; Zibblefrotz.
            frotz))
  • 对于完整的句子的注释,句首字母应该大写,句与句之间用一个空格分隔。 one space. [链接]

  • 避免冗余的注释。 [链接]

    ;; 糟糕
    (inc counter) ; increments counter by one
  • 注释要和代码同步更新。过期的注释还不如没有注释。 at all. [链接]

  • 有时,使用 #_ 宏要优于普通的注释 [链接]

    ;; 很好
    (+ foo #_(bar x) delta)
    
    ;; 糟糕
    (+ foo
       ;; (bar x)
       delta)

好的代码和好的笑话一样,不需要额外的解释。
-- Russ Olsen

  • 避免使用注释去描述一段写得很糟糕的代码。重构它,让它更为可读。(做或者不做,没有尝试这一说。--Yoda) [链接]

注释中的标识

  • 标识应该写在对应代码的上一行。 [链接]

  • 标识后面是一个冒号和一个空格,以及一段描述文字。 [链接]

  • 如果标识的描述文字超过一行,则第二行需要进行缩进。 [链接]

  • 将自己姓名的首字母以及当前日期附加到标识描述文字中 [链接]

    (defn some-fun
      []
      ;; FIXME: 这段代码在v1.2.3之后偶尔会崩溃。
      ;;        这可能和升级BarBazUtil有关。(xz 13-1-31)
      (baz))
  • 对于功能非常明显,实在无需添加注释的情况,可以在行尾添加一个标识。 [链接]

    (defn bar
      []
      (sleep 100)) ; OPTIMIZE
  • 使用 TODO 来表示需要后期添加的功能或特性。 [链接]

  • 使用 FIXME 来表示需要修复的问题。 [链接]

  • 使用 OPTIMIZE 来表示会引起性能问题的代码,并需要修复。 [链接]

  • 使用 HACK 来表示这段代码并不正规,需要在后期进行重构。 [链接]

  • 使用 REVIEW 来表示需要进一步审查这段代码,如:REVIEW: 你确定客户会正确地操作X吗? [链接]

  • 可以使用其它你认为合适的标识关键字,但记得一定要在项目的 README 文件中描述这些自定义的标识。 [链接]

惯用法

  • 使用函数式风格进行编程,避免改变变量的值。 [链接]

  • 保持编码风格。 [链接]

  • 用正常人的思维来思考。 [链接]

工具

这里有一些由Clojure的社区创建的工具,可能会帮助你 在你努力写出地道的Clojure代码。

  • Slamhound是一种能自动从你的现有的代码生成合适的 ns 声明。
  • kibit 是一个用于Clojure的静态代码分析器。 core.logic 为代码搜索可能存在一个更惯用模式函数或宏。

贡献

本文中的所有内容都还没有最后定型,我很希望能够和所有对Clojure代码规范感兴趣的同仁一起编写此文,从而形成一份对社区有益的文档。

你可以随时创建讨论话题,或发送合并申请。我在这里提前表示感谢。

You can also support the style guide with financial contributions via gittip.

Support via Gittip

证书

创作共用许可 这项工作是根据 知识共享署名3.0 本地化许可协议 许可。

宣传

一份由社区驱动的代码规范如果得不到社区本身的支持和认同,那它就毫无意义了。微博转发这份指南,分享给你的朋友或同事。我们得到的每个注解、建议或意见都可以让这份指南变得更好一点。而我们想要拥有的是最好的指南,不是吗?

Cheers,
Bozhidar

You can’t perform that action at this time.