Skip to content
Permalink
master
Switch branches/tags
Go to file
2 contributors

Users who have contributed to this file

Loading
@pn-y @Nondv

The Clojure Style Guide

Примечание переводчика

Это перевод оригинального руководства, составленного Божидаром Бацовым (Bozhidar Batsov). У меня возникли некоторые сложности с переводом некоторых терминов, поэтому я сразу привожу их список:

  • forms - формы (например, special forms - специальные формы)
  • body - тело (как в выражении "тело функции").
  • binding - связывание, связка (например, local binding - локальное связывание, two-way binding - двустороннее связывание, let-bindings - связки let)
  • keywords - ключи (:whatever).
  • hashmap, map - хеш ({:key value})
  • docstring - док. строка
  • argument vector - параметры, список параметров (например, в определении (defn f [x y] ...) argument vector - это [x y]). Важно! в контексте определения функции я использую термин параметр, а в контексте вызова - аргумент.
  • multi-arity - многоарность, многоарная (функция)
  • top-level - верхний уровень, верхнеуровневый
  • forward reference - раннее определение
  • transient - изменяемые. см. документацию
  • будет дополняться

Также я иногда буду оставлять в скобках оригинал выражения/термина.

Введение

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

Данное руководство предлагает наилучшие рекомендации по оформлению Clojure кода для того, чтобы Clojure-программисты могли писать код, который смогут поддерживать другие Clojure-программисты. Руководство по оформлению кода, отражающее практики, используемые в реальном мире, будет использоваться людьми, а руководство, придерживающееся отвергнутых людьми, которым он должен помочь, рискует не быть используемым ими вовсе (не важно, насколько он при этом хорош).

Руководство поделено на несколько разделов, объединяющих связанные между собой правила. Я постарался добавить пояснения к правилам (если же они опущены, значит, я предположил, что они и так очевидны и в пояснениях не нуждаются).

Я не взял все эти идеи с потолка. Они основаны по большей части на моем личном обширном опыте разработки, комментариях и предложениях членов Clojure сообщества и некоторых авторитетных источников информации по Clojure, например, "Clojure Programming" и "The Joy of Clojure".

Руководство все еще разрабатывается: некоторые разделы отсутствуют, некоторые все еще не завершены, каким-то правилам не хватает хороших примеров. Когда-нибудь эти недостатки будут устранены, а пока просто держите их в уме.

Обратите внимание, что разработчики Clojure также поддерживают список стандартов по разработке библиотек

Вы можете сгенерировать PDF или HTML копию данного руководства с помощью Pandoc.

Переводы доступны на следующих языках:

Содержание

Организация кода

Почти каждый уверен, что все стили кода, кроме его собственного, - уродливые и нечитаемые. Если убрать "кроме его собственного", то, возможно, они правы...
-- Jerry Coffin (об отступах)

  • Используйте пробелы для отступов. Никаких табов. [link]

  • Используйте 2 пробела для тела формы. Формы с телом включают def-подобные конструкции, специальные формы и макросы, вводящие локальное связывание (например, loop, let, when-let) и многие макросы вроде when, cond, as->, cond->, case, with-* и т.д. [link]

    ;; хорошо
    (when something
      (something-else))
    
    (with-out-str
      (println "Hello, ")
      (println "world!"))
    
    ;; плохо - четыре пробела
    (when something
        (something-else))
    
    ;; плохо - один пробел
    (with-out-str
     (println "Hello, ")
     (println "world!"))
  • Выравнивайте по вертикали аргументы функции (макроса), если они расположены на нескольких строках. [link]

    ;; хорошо
    (filter even?
            (range 1 10))
    
    ;; плохо
    (filter even?
      (range 1 10))
  • Используйте отступ в один пробел для аргументов функции (макроса), если на одной строке с именем функции их (аргументов) нет. [link]

    ;; хорошо
    (filter
     even?
     (range 1 10))
    
    (or
     ala
     bala
     portokala)
    
    ;; плохо - два пробела
    (filter
      even?
      (range 1 10))
    
    (or
      ala
      bala
      portokala)
  • Выравнивайте по вертикали let-связки (переменные) и ключи для хешей [link]

    ;; хорошо
    (let [thing1 "some stuff"
          thing2 "other stuff"]
      {:thing1 thing1
       :thing2 thing2})
    
    ;; плохо
    (let [thing1 "some stuff"
      thing2 "other stuff"]
      {:thing1 thing1
      :thing2 thing2})
  • Можете не использовать перенос строки между именем функции и ее параметрами в defn, если отсутствует док. строка. [link]

    ;; хорошо
    (defn foo
      [x]
      (bar x))
    
    ;; хорошо
    (defn foo [x]
      (bar x))
    
    ;; плохо
    (defn foo
      [x] (bar x))
  • Размещайте dispatch-val (см. defmethod) на той же строке, что и имя функции. [link]

    ;; хорошо
    (defmethod foo :bar [x] (baz x))
    
    (defmethod foo :bar
      [x]
      (baz x))
    
    ;; плохо
    (defmethod foo
      :bar
      [x]
      (baz x))
    
    (defmethod foo
      :bar [x]
      (baz x))
  • Можете опустить перенос строки между списком параметров и коротким телом функции [link]

    ;; хорошо
    (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)))
  • Для каждой арности функции расставляйте отступы в соответствии с параметрами. [link]

    ;; хорошо
    (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) арности предоставляют возможность передать функции переменное количество аргументов (например, с помощью &). [link]

    ;; хорошо - легко найти 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)))
  • Используйте Unix окончания строк. (Пользователи *BSD/Solaris/Linux/OSX используют их по-умолчанию, а пользователям Windows нужно проявлять осторожность). [link]

    • Если вы используете Git, то можете добавить следующую настройку в конфигурацию своего проекта, чтобы уберечь себя от Windows-переносов:
    bash$ git config --global core.autocrlf true
    
  • Если какой-либо текст предшествует открытой скобке ((, {, [) или следует за закрытой скобкой, то отделяйте его от скобки пробелом. И наоборот, не добавляйте пробелов между открытой скобки и последующим текстов и между текстом и последующей скобкой [link]

    ;; хорошо
    (foo (bar baz) quux)
    
    ;; плохо
    (foo(bar baz)quux)
    (foo ( bar baz ) quux)

Синтаксический сахар вызывает рак точек с запятой (semicolon cancer). -- Alan Perlis

  • Не используйте запятые между элементами последовательностей (списки, векторы) [link]

    ;; хорошо
    [1 2 3]
    (1 2 3)
    
    ;; плохо
    [1, 2, 3]
    (1, 2, 3)
  • Подумайте об улучшении читаемости хеш-литералов с помощью использования запятых и переносов. [link]

    ;; хорошо
    {:name "Bruce Wayne" :alter-ego "Batman"}
    
    ;; хорошо и в некотором роде более читаемо
    {:name "Bruce Wayne"
     :alter-ego "Batman"}
    
    ;; хорошо и при этом более компактно (чем предыдущий пример)
    {:name "Bruce Wayne", :alter-ego "Batman"}
  • Оставляйте конечные скобки на одной строке [link]

    ;; хорошо - на одной строке
    (when something
      (something-else))
    
    ;; плохо - на разных строках
    (when something
      (something-else)
    )
  • Оставляйте пустые строки между верхнеуровневыми формами [link]

    ;; хорошо
    (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. [link]

  • Старайтесь избегать написания строк длиннее 80 символов [link]

  • Избегайте лишних пробелов в конце (строк) [link]

  • Для каждого пространства имен создавайте отдельный файл [link]

  • Определяйте каждое пространство имен с помощью ns с использованием :refer, :require и :import. Желательно, в таком порядке. [link]

    (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
                                     LinkedBlockingQueue]))
  • В ns предпочитайте :require :as вместо :require :refer и, тем более, :require :refer :all. Также предпочитайте :require вместо :use. Последнее должно считаться устаревшим в новом коде. [link]

    ;; хорошо
    (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))
  • Старайтесь не использовать односоставные пространства имен. [link]

    ;; хорошо
    (ns example.ns)
    
    ;; плохо
    (ns example)
  • Избегайте не использовать слишком длинные названия пространства имен (т.е. состоящие более чем из 5 частей). [link]

  • Избегайте функций, состоящих из более 10 строк. В идеале, большинство функций должны быть менее 5 строк длиной. [link]

  • Избегайте создания функций с более чем тремя-четырьмя позиционными параметрами. (прим. переводчика: [x y & more] - это всего три параметра, хотя аргументов можно передать гораздо больше) [link]

  • Не используйте ранние определения (forward references). Иногда они необходимы, то это довольно редкий случай на практике. [link]

Syntax

  • Избегайте использования функций, влияющих на пространство имен, например, require и refer. Они совершенно не нужны, если вы не находитесь в REPL. [link]

  • Используйте declare, когда вам необходимы ранние определения. [link]

  • Старайтесь использовать функции высшего порядка (такие как map) вместо loop/recur. [link]

  • Старайтесь использовать пред- и пост- условия функции вместо проверок внутри тела функции [link]

    ;; хорошо
    (defn foo [x]
      {:pre [(pos? x)]}
      (bar x))
    
    ;; плохо
    (defn foo [x]
      (if (pos? x)
        (bar x)
        (throw (IllegalArgumentException. "x must be a positive number!")))
  • Don't define vars inside functions. [link]

    ;; очень плохо
    (defn foo []
      (def x 5)
      ...)
  • Не перекрывайте имена из clojure.core локальными именами. [link]

    ;; плохо - если вам понадобится функция map, вам придется использовать ее
    ;; с помощью clojure.core/map
    (defn foo [map]
      ...)
  • Используйте alter-var-root вместо def для изменения значения переменной. [link]

    ;; хорошо
    (def thing 1)
    ; какие-то действия над thing
    (alter-var-root #'thing (constantly nil)) ; значение thing теперь nil
    
    ;; плохо
    (def thing 1)
    ; какие-то действия над thing
    (def thing nil) ; значение thing теперь nil
  • Используйте seq, когда нужно проверить, что последовательность пуста (этот прием иногда называется nil punning). [link]

    ;; хорошо
    (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, когда вам нужно превратить последовательность в вектор. [link]

    ;; хорошо
    (vec some-seq)
    
    ;; плохо
    (into [] some-seq)
  • Используйте when вместо (if ... (do ...). [link]

    ;; хорошо
    (when pred
      (foo)
      (bar))
    
    ;; плохо
    (if pred
      (do
        (foo)
        (bar)))
  • Используйте if-let вместо let + if. [link]

    ;; хорошо
    (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. [link]

    ;; хорошо
    (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 ...) ...). [link]

    ;; хорошо
    (if-not pred
      (foo))
    
    ;; плохо
    (if (not pred)
      (foo))
  • Используйте when-not вместо (when (not ...) ...). [link]

    ;; хорошо
    (when-not pred
      (foo)
      (bar))
    
    ;; плохо
    (when (not pred)
      (foo)
      (bar))
  • Используйте when-not вместо (if-not ... (do ...). [link]

    ;; хорошо
    (when-not pred
      (foo)
      (bar))
    
    ;; плохо
    (if-not pred
      (do
        (foo)
        (bar)))
  • Используйте not= Вместо (not (= ...)). [link]

    ;; хорошо
    (not= foo bar)
    
    ;; плохо
    (not (= foo bar))
  • Используйте printf вместо (print (format ...)). [link]

    ;; хорошо
    (printf "Hello, %s!\n" name)
    
    ;; сойдет
    (println (format "Hello, %s!" name))
  • При сравнениях не забывайте, что функции <, > и т.д. принимают переменное количество аргументов. [link]

    ;; хорошо
    (< 5 x 10)
    
    ;; плохо
    (and (> x 5) (< x 10))
  • Используйте % вместо %1 в литералах функции с одним параметром [link]

    ;; хорошо
    #(Math/round %)
    
    ;; плохо
    #(Math/round %1)
  • Используйте %1 вместо % в литералах функций с несколькими параметрами. [link]

    ;; хорошо
    #(Math/pow %1 %2)
    
    ;; плохо
    #(Math/pow % %2)
  • Не оборачивайте функции в анонимные функции, если это не требуется. [link]

    ;; хорошо
    (filter even? (range 1 10))
    
    ;; плохо
    (filter #(even? %) (range 1 10))
  • Не используйте литералы функций, если тело функции будет состоять из более чем одной формы. [link]

    ;; хорошо
    (fn [x]
      (println x)
      (* x 2))
    
    ;; плохо (you need an explicit do form)
    #(do (println %)
         (* % 2))
  • Предпочитайте complement вместо использования анонимной функции. [link]

    ;; хорошо
    (filter (complement some-pred?) coll)
    
    ;; плохо
    (filter #(not (some-pred? %)) coll)

    Это правило, очевидно, должно быть проигнорированно, если дополнение функции уже существует в форме другой функции (например, even? и odd?).

  • Используйте comp, когда это упрощает код. [link]

    ;; Предполагается `(:require [clojure.string :as str])`...
    
    ;; хорошо
    (map #(str/capitalize (str/trim %)) ["top " " test "])
    
    ;; лучше
    (map (comp str/capitalize str/trim) ["top " " test "])
  • Используйте partial, когда это упрощает код. [link]

    ;; хорошо
    (map #(+ 5 %) (range 1 10))
    
    ;; (кажется) лучше
    (map (partial + 5) (range 1 10))
  • Используйте threading макрос -> (thread-first) and ->> (thread-last) в глубоко вложенных формах. [[link](#- `` - hreading-macros)]

    ;; хорошо
    (-> [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)))
  • Используйте :else, когда нужно перехватить значение, не проходящее остальные условия. [link]

    ;; хорошо
    (cond
      (neg? n) "negative"
      (pos? n) "positive"
      :else "zero")
    
    ;; плохо
    (cond
      (neg? n) "negative"
      (pos? n) "positive"
      true "zero")
  • Предпочитайте condp вместо cond, когда предикатное выражение не меняется. [link]

    ;; хорошо
    (cond
      (= x 10) :ten
      (= x 20) :twenty
      (= x 30) :thirty
      :else :dunno)
    
    ;; намного лучше
    (condp = x
      10 :ten
      20 :twenty
      30 :thirty
      :dunno)
  • Предпочитайте case вместо cond и condp, когда условные выражения - константы, известные еще на этапе компиляции. [link]

    ;; хорошо
    (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 и подобных. Если это невозможно, то визуально разделите формы на пары с помощью комментариев и пустых строк. [link]

    ;; хорошо
    (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 как предикат, если возможно. [link]

    ;; хорошо
    (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). [link]

  • Используйте (pos? x), (neg? x) и (zero? x) вместо (> x 0), (< x 0) и (= x 0). [link]

  • Используйте list* вместо последовательных вызовов cons. [link]

    ;; хорошо
    (list* 1 2 3 [4 5])
    
    ;; плохо
    (cons 1 (cons 2 (cons 3 [4 5])))
  • Используйте синт. сахар для работы с Java. [link]

    ;;; создание объекта
    ;; хорошо
    (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)
  • Используйте краткий синтаксис для описания метаданных, если они содержат только ключи и только значения true. [link]

    ;; хорошо
    (def ^:private a 5)
    
    ;; плохо
    (def ^{:private true} a 5)
  • Помечайте приватные части кода. [link]

    ;; хорошо
    (defn- private-fun [] ...)
    
    (def ^:private private-var ...)
    
    ;; плохо
    (defn private-fun [] ...) ; не приватная
    
    (defn ^:private private-fun [] ...) ; слишком многословно
    
    (def private-var ...) ; не приватная
  • Для доступа к приватной переменной (например, в рамках тестирования) используйте форму @#'some.ns/var. [link]

  • Будьте внимательны с тем, к чему конкретно вы привязываете метаданные. [link]

    ;; мы привязываем метаданные к переменной `a`
    (def ^:private a {})
    (meta a) ;=> nil
    (meta #'a) ;=> {:private true}
    
    ;; мы привязываем метаданные к хешу
    (def a ^:private {})
    (meta a) ;=> {:private true}
    (meta #'a) ;=> nil

Именование

Единственными сложностями программирования являются проверка актуальности кеша и именование. -- Phil Karlton

  • Когда именуете пространство имен, предпочитайте следующие шаблоны: [link]

    • проект.модуль
    • компания.проект.модуль
  • Используйте lisp-case в отдельных частях пространства имен (например, bruce.project-euler) [link]

  • Используйте lisp-case для имен функций и переменных. [link]

    ;; хорошо
    (def some-var ...)
    (defn some-fun ...)
    
    ;; плохо
    (def someVar ...)
    (defn somefun ...)
    (def some_fun ...)
  • Используйте CamelCase для протоколов, записей, структур и типов. Сохраняйте акронимы вроде HTTP, RFC, XML в верхнем регистре. [link]

  • Имена предикатов (функций, возвращающих булевое значение) должны оканчиваться вопросительным знаком (например, even?). [link]

    ;; хорошо
    (defn palindrome? ...)
    
    ;; плохо
    (defn palindrome-p ...) ; стиль Common Lisp
    (defn is-palindrome ...) ; стиль Java
  • Имена функций/макросов, которые небезопасны в транзакциях STM должны оканчиваться восклицательным знаком (например, reset!) [link]

  • Используйте -> вместо to в названиях приводящих (к типу) функций. [link]

    ;; хорошо
    (defn f->c ...)
    
    ;; не так хорошо
    (defn f-to-c ...)
  • Используйте *звездочки* (в ориг. earmuffs) для того, что планируется переопределить. [link]

    ;; хорошо
    (def ^:dynamic *a* 10)
    
    ;; плохо
    (def ^:dynamic a 10)
  • Не используйте никакой особенной записи для констант - предполагается, что все есть константы, если не указано обратное. [link]

  • Используйте _ при именовании параметров (в т.ч. при деструктурировании), которые не будут использоваться в коде. [link]

    ;; хорошо
    (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 и используйте типичные имена вроде pred и coll. [link]

    • в функциях:
      • f, g, h - для функций
      • n - целочисленный параметр (обычно размер)
      • index, i - целочисленный индекс
      • x, y - числа
      • xs - последовательность
      • m - хеш
      • s - строка
      • re - регулярное выражение
      • coll - коллекция
      • pred - предикат
      • & more - при переменном числе аргументов
      • xf - xform, transducer
    • в макросах:
      • expr - выражение
      • body - тело макроса
      • binding - связки макроса (список параметров)

Коллекции

Лучше иметь 100 функций, работающих с одной структурой данных нежели 10 функций, работающих с 10 структурами
-- Alan J. Perlis

  • Избегайте использования списков для хранения данных (разве что список - это именно то, что вам нужно) [link]

  • Старайтесь использовать ключевые слова (ключи, keywords) в качестве ключей хеша. [link]

    ;; хорошо
    {:name "Bruce" :age 30}
    
    ;; плохо
    {"name" "Bruce" "age" 30}
  • Старайтесь использовать литералы коллекций, если возможно. Однако для множеств используйте литералы только в том случае, когда значениями являются константы, известные на этапе компиляции. [link]

    ;; хорошо
    [1 2 3]
    #{1 2 3}
    (hash-set (func1) (func2)) ; значения определяются во время выполнения
    
    ;; плохо
    (vector 1 2 3)
    (hash-set 1 2 3)
    #{(func1) (func2)} ; во время выполнения выбросит исключение, если (func1) = (func2)
  • Старайтесь не обращаться к членам коллекций по индексу, если возможно. [link]

  • Старайтесь использовать ключи как функции для доступа к значениям хешей, если возможно. [link]

    (def m {:name "Bruce" :age 30})
    
    ;; хорошо
    (:name m)
    
    ;; слишком многословно
    (get m :name)
    
    ;; плохо - возможна ошибка NullPointerException
    (m :name)
  • Используйте факт, что большинство коллекций являются функциями. [link]

    ;; хорошо
    (filter #{\a \e \o \i \u} "this is a test")
    
    ;; плохо - слишком страшно, чтобы показывать
  • Используйте факт, что ключи могут быть использованы как функции над коллекцией. [link]

    ((juxt :a :b) {:a "ala" :b "bala"})
  • Не используйте изменяемые (transient) коллекции, если только это не критическая часть кода, требующая максимального быстродействия. [link]

  • Избегайте использования коллекций из Java. [link]

  • Избегайте использования Java массивов. Разве что для случаев интеграции и критического кода, много работающего с примитивами. [link]

Изменение состояния

Транзакционные ссылки (Refs)

  • Подумайте над оборачиванием всех IO вызовов макросом io!, чтобы избежать гадких сюрпризов, если вы случайно запустите этот код внутри транзакции [link]

  • Избегайте использования ref-set, если возможно. [link]

    (def r (ref 0))
    
    ;; хорошо
    (dosync (alter r + 5))
    
    ;; плохо
    (dosync (ref-set r 5))
  • Старайтесь держать размер транзакций (количество работы, инкапсулированное в них) настолько маленьким насколько возможно. [link]

  • Избегайте наличия коротких и длительных транзакций, работающих с одной и той же ссылкой. [link]

Агенты

  • Используйте send только для действий, привязанных к процессору и не блокирующих IO или другие потоки. [link]

  • Используйте send-off для действий, которые могут заблокировать, "усыпить" (sleep) или как-то иначе подвесить поток. [link]

Атомы

  • Избегайте обновления атомов внутри STM транзакции. [link]

  • Старайтесь использовать swap! вместо reset!, где возможно. [link]

    (def a (atom 0))
    
    ;; хорошо
    (swap! a + 5)
    
    ;; Не так хорошо
    (reset! a 5)

Строки

  • Старайтесь использовать для работы со строками функции из clojure.string вместо интеграции с Java или введения собственных. [link]

    ;; хорошо
    (clojure.string/upper-case "bruce")
    
    ;; плохо
    (.toUpperCase "bruce")

Исключения

  • Используйте существующие исключения. Хороший Clojure код выбрасывает исключения стандартных типов (если выбрасывает). Например, java.lang.IllegalArgumentException, java.lang.UnsupportedOperationException, java.lang.IllegalStateException, java.io.IOException. [link]

  • Используйте with-open вместо finally. [link]

Макросы

  • Не пишите макросы, если хороша и функция. [link]

  • Перед макросом напишите пример его использования. [link]

  • Разбивайте сложные макросы на меньшие функции. [link]

  • Макрос обычно должен быть всего-лишь синтаксическим сахаром над обычной функций. В таком случае вам будет легче комбинировать функции. [link]

  • Старайтесь использовать syntax-quote (`) вместо того, чтобы создавать список "вручную". [link]

Комментарии

Хороший код является лучшей документацией для самого себя. Если вы собираетесь добавить комментарий, то спросите себя: "Как я могу улучшить свой код, чтобы этот комментарий был не нужен?". Улучшите код и задокументируйте его, чтобы он стал еще более чистым
-- Steve McConnell

  • Старайтесь писать самодокументируемый код. [link]

  • Пишите заглавный комментарий минимум с четырьмя точками с запятой (;;;;). [link]

  • Пишите комментарии верхнего уровня с тремя точками с запятой (;;;). [link]

  • Пишите комментарии к фрагменту кода перед ним и выравнивайте их, используя две точки с запятой (;;). [link]

  • Пишите боковые комментарии, начиная с одной точки с запятой (;). [link]

  • Всегда оставляйте как минимум один пробел после точек с запятой. [link]

    ;;;; 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))
  • Комментарии, состоящие из более чем одного слова, начинаются с большой буквы и используют пунктуацию. Отделяйте предложения одним пробелом. [link]

  • Не оставляйте очевидных комментариев [link]

    ;; плохо
    (inc counter) ; увеличивает counter на единицу
  • Комментарии должны быть актуальными. Устаревший комментарий хуже, чем отсутствие комментария вообще. [link]

  • Используйте макрос #_ вместо обычного комментирования, когда вам нужно закомментировать форму. [link]

    ;; хорошо
    (+ foo #_(bar x) delta)
    
    ;; плохо
    (+ foo
       ;; (bar x)
       delta)

Хороший код, как и хорошая шутка, не требует объяснения.
-- Russ Olsen

  • Старайтесь не писать коммментарии, чтобы объяснить плохой код. Измените код, чтобы он стал понятным ("Делай или не делай, не надо пытаться." --Йода). [link]

Аннотации (Comment Annotations)

  • Аннотации обычно должны размещаться непосредственно над связанным кодом. [link]

  • Между ключевым словом аннотации и описанием должны стоять двоеточие и пробел. Например: TODO: add something cool. [link]

  • Если описание занимает несколько строк, то каждая строка должна быть выровнена в соответствии с первой (т.е. после ключевого слова). [link]

  • Помечайте аннотацию своими инициалами и датой, чтобы актуальность было легче отследить. [link]

    (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))
  • В случае, когда проблема настолько очевидна, что ее описание будет лишним, аннотации могут быть оставлены в конце проблемной строки в виде ключевого слова без пояснения. Такое использование должно быть исключением, но не правилом. [link]

    (defn bar
      []
      (sleep 100)) ; OPTIMIZE
  • Используйте TODO, чтобы отметить недостающие фичи или функциональность, которую нужно будет добавить позже. [link]

  • Используйте FIXME, чтобы отметить сломанный код, который нужно исправить. [link]

  • Используйте OPTIMIZE, чтобы отметить медленный или неэффективный код, который может вызвать проблемы с производительностью. [link]

  • Используйте HACK, чтобы отметить "пахнущий" код (костыли, прим. переводчика), который использует сомнительные приемы и должен быть отрефакторен. [link]

  • Используйте REVIEW, чтобы отметить то, с чем нужно внимательнее ознакомиться, чтобы убедиться, что оно работает так, как нужно. Например: REVIEW: Are we sure this is how the client does X currently? [link]

  • Используйте другие ключевые слова для аннотаций, если они кажутся приемлемыми. Но не забудьте добавить их в README вашего проекта. [link]

Документирование

Док. строки - основной способ документирования Clojure кода. Многие формы-определения (например, def, defn, defmacro, ns) поддерживают док. строки и использовать их - хорошая идея вне зависимости от того, является ли описываемое публичным или приватным.

Если форма-определение не поддерживает док. строки, то вы все равно можете добавить документация с помощью аттрибута метаданных :doc.

В этом разделе выделены некоторые общие практики по документированию Clojure кода.

  • Если возможно, используйте док. строки вместо :doc. [link]
;; хорошо
(defn foo
  "This function doesn't do much."
  []
  ...)

(ns foo.bar.core
  "That's an awesome library.")

;; плохо
(defn foo
  ^{:doc "This function doesn't do much."}
  []
  ...)

(ns ^{:doc "That's an awesome library.")
  foo.bar.core)
  • Пишите док. строки законченными предложениями с большой буквы, разумно описывающими сущность, к которой привязаны. Благодаря этому ваши инструменты (текстовые редакторы и IDE) смогут отображать краткое описание сущности во многих местах. [link]
;; хорошо
(defn frobnitz
  "This function does a frobnitz.
  It will do gnorwatz to achieve this, but only under certain
  cricumstances."
  []
  ...)

;; плохо
(defn frobnitz
  "This function does a frobnitz. It will do gnorwatz to
  achieve this, but only under certain cricumstances."
  []
  ...)
  • Документируйте все позиционные параметры и оборачивайте их с помощью обратных ковычек (`). Таким образом, текстовые редакторы и IDE смогут определять их (параметры) и предоставлять дополнительную функциональность для них. [link]
;; хорошо
(defn watsitz
  "Watsitz takes a `frob` and converts it to a znoot.
  When the `frob` is negative, the znoot becomes angry."
  [frob]
  ...)

;; плохо
(defn watsitz
  "Watsitz takes a frob and converts it to a znoot.
  When the frob is negative, the znoot becomes angry."
  [frob]
  ...)
  • Оборачивайте ссылки на другие сущности (функции, константы) в обратные `ковычки`, чтобы ваши инструменты могли определять их. [link]
;; хорошо
(defn wombat
  "Acts much like `clojure.core/identity` except when it doesn't.
  Takes `x` as an argument and returns that. If it feels like it."
  [x]
  ...)

;; плохо
(defn wombat
  "Acts much like clojure.core/identity except when it doesn't.
  Takes `x` as an argument and returns that. If it feels like it."
  [x]
  ...)
  • Док.строки должны быть грамотно оформленными предложениями на английском языке, т.е. каждое предложение должно начинаться с прописной буквы и заканчиваться точкой (или другим подходящим знаком препинания). Предложения должны отделяться пробелом. [link]
;; хорошо
(def foo
  "All sentences should end with a period (or maybe an exclamation mark).
  And the period should be followed by a space, unless it's the last sentence.")

;; плохо
(def foo
  "all sentences should end with a period (or maybe an exclamation mark).
  And the period should be followed by a space, unless it's the last sentence")
  • Оставляйте отступ в два пробела в многострочных док. строках. [link]
;; хорошо
(ns my.ns
  "It is actually possible to document a ns.
  It's a nice place to describe the purpose of the namespace and maybe even
  the overall conventions used. Note how _not_ indenting the doc string makes
  it easier for tooling to display it correctly.")

;; плохо
(ns my.ns
  "It is actually possible to document a ns.
It's a nice place to describe the purpose of the namespace and maybe even
the overall conventions used. Note how _not_ indenting the doc string makes
it easier for tooling to display it correctly.")
  • Не начинайте и не заканчивайте док. строки пробелом. [link]
;; хорошо
(def foo
  "I'm so awesome."
  42)

;; плохо
(def silly
  "    It's just silly to start a doc string with spaces.
  Just as silly as it is to end it with a bunch of them.      "
  42)
  • Когда пишете док. строку, размещайте его после имени функции, а не после списка параметров. Последнее не является нарушением синтаксиса и не вызовет никаких ошибок, но включает строку как часть тела функции без привязки к документации. [link]
;; хорошо
(defn foo
  "docstring"
  [x]
  (bar x))

;; плохо
(defn foo [x]
  "docstring"
  (bar x))

Общие рекомендации

  • Пишите в функциональном стиле, используя изменяемое состояние тогда, когда это необходимо. [link]

  • Будьте последовательны. В идеале, будьте последовательны по этому руководству. [link]

  • Используйте здравый смысл. [link]

Инструменты

Есть несколько полезных штук, созданных Clojure сообществом, которые могут помочь вам в вашем стремлении писать хороший Clojure код.

  • Slamhound - инструмент, который будет генерировать верные ns определения из существующего кода.

  • kibit - статический анализатор кода для Clojure, который использует core.logic для поиска кода, который можно переписать с помощью существующей функции или макроса.

Тестирование

  • Храните ваши тесты в отдельной директории, обычно test/yourproject (в контрасте с src/yourproject). Ваш инструмент сборки (build tool) должен обеспечить доступ к ним, когда это необходимо. Большинство шаблонов работают с тестами автоматически. [link]

  • Именуйте пространство имен теста как yourproject.something-test, файл обычно будет выглядеть как test/yourproject/something_test.clj (или .cljc, cljs). [link]

  • При использовании clojure.test, определяйте ваши тесты с помощью deftest и называйте их something-test. [link]

    ;; хорошо
    (deftest something-test ...)
    
    ;; плохо
    (deftest something-tests ...)
    (deftest test-something ...)
    (deftest something ...)

Создание библиотек

  • Если вы публикуете библиотеки, используемые другими людьми, обязательно следуйте руководству Central Repository при выборе groupId и artifactId. Это позволит предотвратить конфликты имен и облегчить широкое ее использование. Хорошим примером является Component. [link]

  • Избегайте ненужных зависимостей. Например, трехстрочная вспомогательная функция, скопированная в проект обычно лучше, чем зависисмость, тянущая за собой сотни наименований, которые вы не собираетесь использовать. [link]

  • Предоставляйте основную функциональность и различные интеграции в отдельных артефактах. В таком случае, пользователи смогут воспользоваться вашей библиотекой, не ограничивая себя вашими инструментальными предпочтениями. Например, Component предоставляет основной функционал, а reloaded предоставляет интеграцию с leiningen. [link]

Помощь проекту

Написанное в этом руководстве не является истинной последней инстанции. Я хочу работать совместно с каждым, кто заинтересован в хорошем оформлении кода Clojure, чтобы мы смогли создать ресурс, полезный для всего Clojure сообщества.

Не стесняйтесь открывать тикеты и создавать pull-реквесты с улучшениями. Заранее спасибо за помощь!

Также вы можете помочь руководству финансово с помощью gittip.

Support via Gittip

Примечание переводчика:

Данный репозиторий содержит лишь перевод оригинального руководства. Соответственно, все вопросы и предложения, не касающиеся конкретно перевода, должны направляться в оригинальный репозиторий.

License

Creative Commons License Данное руководство использует лицензию Creative Commons Attribution 3.0 Unported License

Расскажите всем

Руководство, написанное сообществом, не может оказывать много помощи сообществу, которое не знает о его существовании. Расскажите о данном гайде друзьям и коллегам. Каждый комментарий, каждое предложение и мнение, которые мы получаем, делают это руководство немного лучше. А мы хотим иметь наилучшее возможное руководство, не правда ли?

Удачи,
Божидар