首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > 开发语言 > 编程 >

Clojure-作派指南

2013-11-18 
Clojure-风格指南原文:clojure-style-guide译者:JuanitoFatas?转载:http://lisp.tw/2013/02/19/clojure-st

Clojure-风格指南

原文:clojure-style-guide

译者:JuanitoFatas

?转载:http://lisp.tw/2013/02/19/clojure-style-guide/

Clojure 风格指南

这篇 Clojure 风格指南向你推荐现实世界中的最佳实践,Clojure 程序员如何写出可被别的 Clojure 程序员维护的代码。一份风格指南反映出现实世界中的用法,并带有一个理想,避免已经公认是危险的事物被人继续使用,不管看起来是多么的好。

本指南依照相关规则分成数个小节。我尽力在规则之后说明理由(如果省略的话,我相信理由是显而易见的)。

我没有想到所有的规则 —– 他们大致上是基于,我作为一个专业软体工程师的广泛生涯,从 Clojure 社群成员所得到的反馈及建议,和数个高度评价的 Clojure 编程资源,像是?"Clojure Programming"以及?"The Joy of Clojure"。

本指南仍在完善中 –– 缺少某些章节,某些不完整,某些规则缺少例子,某些规则例子演示不够清楚。在完稿时,将会解决这些议题 –– 现在就先记在心上就好。

你可以使用?Transmuter?来产生本指南的一份 PDF 或 HTML 复本。

目录
  • 组织源代码与排版
  • 语法
  • 命名
  • 复合类型
  • Mutation
  • 字串
  • 异常
  • 注解
    • 注释
    • 基本原则
    • 组织源代码与排版

      几乎每人都深信,每一个除了自己的风格都又丑又难读。
      把 "除了自己的" 拿掉,他们或许是对的...
      -- 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 ...)
        • 函数或宏定义中间不要放空行。

        • 可行的场合下,避免每行超过 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)。

          语法
          • 避免使用操作命名空间的函数,像是: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
              • 协议、记录、结构及类型使用驼峰形式(专有缩略词保持大写: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?的范例,如?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 的数组。

                        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 社群创造一个有益的资源。

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

                                            口耳相传

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

                                            ?

                                            ?

                                            ?

                                            ?

热点排行