Clojure Design Idiomsλ︎
An idiom is the Clojure way
Idioms are not Design Patterns found in languages like C++ and Java. Design patterns have a very specific language and structure and carefully design the context in which they are used.
Idioms are very simple in comparision, they are the way Clojure should be used.
Idiomatic Clojure is a term commonly used, commonly as a question. Is this idiomatic Clojure?
Idioms from style guideλ︎
A (WIP) refactor of the idioms from the Clojure Style guide
Dynamic Namespace Manipulationλ︎
TODO: vauge statement with no example
Avoid the use of namespace-manipulating functions like require and
refer. They are entirely unnecessary outside of a REPL
environment.
Forward Referencesλ︎
TODO: vauge statement with no example
Avoid forward references. They are occasionally necessary, but such occasions are rare in practice.
Declareλ︎
Use declare to enable forward references when forward references are
necessary.
Higher-order Functionsλ︎
TODO: expand and provide examples
Prefer higher-order functions like map to loop/recur.
Appropriate Function useλ︎
- let def
- if when cond condp
- Nil Punning (a pun that null is not a value, when it is??) seq empty?
Use let for local namesλ︎
let should be used inside a function definition or to create a local name when experimenting in the REPL.
def defines a symbol name at the scope of the current namespace and therefore is inappropriate to use within a function definition.
Avoid vars inside functions
(defn iterative-function
"Avoid breaking the scope of data in a function by using def"
[args]
(def x 5) ; Avoid using a var as it breaks the scope of the function
...)
x ; evaluating will return the result without going through the function call and could be used anywere the namespace is required, increasing complexity.
Problematic debuging technique
An obscure debug technique is to define a def inside a function definition to capture intermediate values, however, this breaks the scope of Clojure and can lead to incorrect use of Clojure (especially by those new to the language).
Use a debug tool rather than break the scope of Clojure with a def, e.g. Cider Debug or Flowstorm-debugger.
Avoid Shadowing Namesλ︎
If names for local bindings or function definitions are the same as those from common libraries, confusion and errors is greatly increased. It also makes a code refactor unnecessarily complicated.
Shadowing clojure.core names (or other popular libraries) exaserbated the situation.
Libraries that are highly generic use names. Code for applications or services should be specific and use domain language for greater clarity.
RELATED: avoid renaming functions required by name via the
nsdefinition.
Alter Var Bindingλ︎
Use alter-var-root instead of def to change the value of a var.
[source,clojure]
;; 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
Nil Punning [[nil-punning]]λ︎
Use seq as a terminating condition to test whether a sequence is
empty (this technique is sometimes called nil punning).
[source,clojure]
;; 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))))
Converting Sequences to Vectors [[to-vector]]λ︎
Prefer vec over into when you need to convert a sequence into a vector.
[source,clojure]
Converting Something to Booleanλ︎
Use the boolean function if you need to convert something to an actual boolean value (true or false).
[source,clojure]
NOTE: Don't forget that the only values in Clojure that are "falsey" are false and nil. Everything else
will evaluate to true when passed to the boolean function.
You'll rarely need an actual boolean value in Clojure, but it's useful to know how to obtain one when you do.
when vs if [[when-instead-of-single-branch-if]]λ︎
Use when instead of if with just the truthy branch, as in (if condition (something...)) or (if ... (do ...)).
[source,clojure]
if-let [[if-let]]λ︎
Use if-let instead of let + if.
[source,clojure]
;; good
(if-let [result (foo x)]
(something-with result)
(something-else))
;; bad
(let [result (foo x)]
(if result
(something-with result)
(something-else)))
when-let [[when-let]]λ︎
Use when-let instead of let + when.
[source,clojure]
;; good
(when-let [result (foo x)]
(do-something-with result)
(do-something-more-with result))
;; bad
(let [result (foo x)]
(when result
(do-something-with result)
(do-something-more-with result)))
if-not [[if-not]]λ︎
Use if-not instead of (if (not ...) ...).
[source,clojure]
when-not [[when-not]]λ︎
Use when-not instead of (when (not ...) ...).
[source,clojure]
when-not vs if-not [[when-not-instead-of-single-branch-if-not]]λ︎
Use when-not instead of (if-not ... (do ...)).
[source,clojure]
not= [[not-equal]]λ︎
Use not= instead of (not (= ...)).
[source,clojure]
printf [[printf]]λ︎
Prefer printf over (print (format ...)).
[source,clojure]
Flexible Comparison Functionsλ︎
When doing comparisons, leverage the fact that Clojure's functions <,
>, etc. accept a variable number of arguments.
[source,clojure]
Single Parameter Function Literal [[single-param-fn-literal]]λ︎
Prefer % over %1 in function literals with only one parameter.
[source,clojure]
Multiple Parameters Function Literal [[multiple-params-fn-literal]]λ︎
Prefer %1 over % in function literals with more than one parameter.
[source,clojure]
No Useless Anonymous Functions [[no-useless-anonymous-fns]]λ︎
Don't wrap functions in anonymous functions when you don't need to.
[source,clojure]
No Multiple Forms in Function Literals [[no-multiple-forms-fn-literals]]λ︎
Don't use function literals if the function body will consist of more than one form.
[source,clojure]
;; good
(fn [x]
(println x)
(* x 2))
;; bad (you need an explicit do form)
#(do (println %)
(* % 2))
Anonymous Functions vs complement, comp and partialλ︎
Prefer anonymous functions over complement, comp and partial, as this results
in simpler code most of the time.footnote:[You can read more on the subject https://ask.clojure.org/index.php/8373/when-should-prefer-comp-and-partial-to-anonymous-functions[here].]
= complement [[complement]]λ︎
[source,clojure]
= comp [[comp]]λ︎
[source,clojure]
;; Assuming `(:require [clojure.string :as str])`...
;; good
(map #(str/capitalize (str/trim %)) ["top " " test "])
;; okish
(map (comp str/capitalize str/trim) ["top " " test "])
comp is quite useful when composing transducer chains, though.
[source,clojure]
= partial [[partial]]λ︎
[source,clojure]
Threading Macros [[threading-macros]]λ︎
Prefer the use of the threading macros +->+ (thread-first) and +->>+
(thread-last) to heavy form nesting.
[source,clojure]
;; 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)))
Threading Macros and Optional Parenthesesλ︎
Parentheses are not required when using the threading macros for functions having no argument specified, so use them only when necessary.
[source,clojure]
;; good
(-> x fizz :foo first frob)
;; bad; parens add clutter and are not needed
(-> x (fizz) (:foo) (first) (frob))
;; good, parens are necessary with an arg
(-> x
(fizz a b)
:foo
first
(frob x y))
Threading Macros Alignmentλ︎
The arguments to the threading macros +->+ (thread-first) and +->>+
(thread-last) should line up.
[source,clojure]
Default cond Branch [[else-keyword-in-cond]]λ︎
Use :else as the catch-all test expression in cond.
[source,clojure]
;; good
(cond
(neg? n) "negative"
(pos? n) "positive"
:else "zero")
;; bad
(cond
(neg? n) "negative"
(pos? n) "positive"
true "zero")
condp vs cond [[condp]]λ︎
Prefer condp instead of cond when the predicate & expression don't
change.
[source,clojure]
;; good
(cond
(= x 10) :ten
(= x 20) :twenty
(= x 30) :thirty
:else :dunno)
;; much better
(condp = x
10 :ten
20 :twenty
30 :thirty
:dunno)
case vs cond/condp [[case]]λ︎
Prefer case instead of cond or condp when test expressions are
compile-time constants.
[source,clojure]
;; 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)
Short Forms In Cond [[short-forms-in-cond]]λ︎
Use short forms in cond and related. If not possible give visual
hints for the pairwise grouping with comments or empty lines.
[source,clojure]
;; good
(cond
(test1) (action1)
(test2) (action2)
:else (default-action))
;; ok-ish
(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 As Predicate [[set-as-predicate]]λ︎
Use a set as a predicate when appropriate.
[source,clojure]
;; good
(remove #{1} [0 1 2 3 4 5])
;; bad
(remove #(= % 1) [0 1 2 3 4 5])
;; good
(count (filter #{\a \e \i \o \u} "mary had a little lamb"))
;; bad
(count (filter #(or (= % \a)
(= % \e)
(= % \i)
(= % \o)
(= % \u))
"mary had a little lamb"))
inc and dec [[inc-and-dec]]λ︎
Use (inc x) & (dec x) instead of (+ x 1) and (- x 1).
pos? and neg? [[pos-and-neg]]λ︎
Use (pos? x), (neg? x) & (zero? x) instead of (> x 0),
(< x 0) & (= x 0).
list* vs cons [[list-star-instead-of-nested-cons]]λ︎
Use list* instead of a series of nested cons invocations.
[source,clojure]
Sugared Java Interop [[sugared-java-interop]]λ︎
Use the sugared Java interop forms.
;;; 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 someField)
Compact Metadata Notation For True Flags [[compact-metadata-notation-for-true-flags]]λ︎
Use the compact metadata notation for metadata that contains only
slots whose keys are keywords and whose value is boolean true.
[source,clojure]
Privateλ︎
Denote private parts of your code.
[source,clojure]
;; good
(defn- private-fun [] ...)
(def ^:private private-var ...)
;; bad
(defn private-fun [] ...) ; not private at all
(defn ^:private private-fun [] ...) ; overly verbose
(def private-var ...) ; not private at all
Access Private Var [[access-private-var]]λ︎
To access a private var (e.g. for testing), use the @#'some.ns/var form.
Attach Metadata Carefully [[attach-metadata-carefully]]λ︎
Be careful regarding what exactly you attach metadata to.
[source,clojure]
;; we attach the metadata to the var referenced by `a`
(def ^:private a {})
(meta a) ;=> nil
(meta #'a) ;=> {:private true}
;; we attach the metadata to the empty hash-map value
(def a ^:private {})
(meta a) ;=> {:private true}
(meta #'a) ;=> nil
Namingλ︎
Using meaningful names in kebab-case for all symbols
- namespaces / names / vars should be concise but not terse
- fully qualified keywords
- naming is hard to do well... k m v
Namespacesλ︎
Use meaningful alias names in require expressions.
Only use refer if the purpose of the current namespace is central the specific functions being referred, e.g. clojure.test
Avoid the use function as it it inters all funcions and can lead to many conflics in function names and other vars.
Documentationλ︎
Function definitions and shared values should include doc-strings that describe the purpose of the function. Arguments and return values should be included in the doc-string
clj-kondo - code quality and idiom checkλ︎
clj-kondo provides live linting and idiom checker within the editor and is also run as a process during continuous integration checks.
Code Analysis clj-kondo GitHub action runs a limited set of lint rules for clj-kondo
- Idiom section of a community style guide
clj-format - deprecated - check code format with rules (quite restrictive, not universally used) - currently used on master, removed in spike/refactor-thinking
kibit - deprecated - the classic Clojure idiom checker - currently used on master, removed in spike/refactor-thinking
Meta dataλ︎
- var scope if required (def, defn) -
^:private - test selectors -
^:integration,^:datastore - depreciated namespaces and functions -
^:deprecated
Macrosλ︎
First rule of macro club... don't write your own macros 😂
Defining your own macros is the path of language design, which has different concerns to application and service design.
If there is no way to satisfy a requirement with function definitions and other vars, then define a macro wisely and ensure the macro has enough worth to merit inclusion into multiple projects.
Coding Styleλ︎
Optimise for readability and consistency
- Long functions / long lines
- Aligning forms
- Avoid the use of declare - refactor code to be less inter-dependent
- Whitespace commit white space /formatting changes separately from application code changes where possible
;;for line comments
NOTE: Cursive setting: Editor > General > Smart Keys > Clojure > Use ;; for line comments
Refactor as you goλ︎
- Review code after tests work for opportunity to refactor
- minimise local name bindings
- consider how understandable code for humans
- ensure there is a doc-string & update if required
Removing redundant codeλ︎
Less code tends to fewer bugs and easier maintenance
- check references to functions
- check for similar functions
- mark code as deprecated if its use is not clear (using a comment or ^deprecated)
- Carve tool to highlight unused vars - example using practicalli/clojure-deps-edn config -
Carveλ︎
Carve will report and remove vars (defn, def, requires) that are in the code but not used.