Lambda Island Clojure Style Guide
This is the style we use on all projects under the lambdaisland banner, as well as projects by Gaiwan, assuming there isn't a client-specific style that we should stick to.
In general we stick to conventions used in the community at large, as encoded in the Clojure Style Guide. This page documents particularities of our style that are either different from the style guide, not covered by the style guide, or generally worth pointing out. It's a continuous work in progress, but mostly because we add or clarify entries. We avoid changing these conventions unless there are extremely compelling arguments to do so.
We avoid bike-shedding over these rules, and don't discuss them unless there are strong arguments that go beyond style and taste. They may be different than what you are used to or like, but that's not a reason to ignore or try to change them.
Ns form
Put :require
/ :import
on the same line as the first entry
;; ✔️ Do this
(ns foo.bar
(:require [hello.world :as world]
[my.cool.thing :as thing]))
;; ❌ not this
(ns foo.bar
(:require
[hello.world :as world]
[my.cool thing :as thing]))
Always use a vector for requires and a list for imports
Wrap namespaces in a vector even when only loading them for side-effects, use the list notation for import even if it's the only class you're importing from a package.
;; ✔️ Do this
(ns foo.bar
(:require [hello.world :as world]
(:import (java.util UUID))
;; ❌ none of this
(ns foo.bar
(:require hello.world
[my.cool [thing :as thing]])
(:import java.util.UUID))
Prefer :as
over :refer
, ideally using the last part of the ns name
So if a namespace is named com.acmeinc.widget
then use :as widget
. Sometimes the last part is too generic though, and you need to use a different name that makes more sense e.g. [hiccup.core :as hiccup]
.
Sometimes the last part is not unique, or not saying much to a person reading the code. In that case you can use a combined alias with hyphens, e.g. [lambdaisland.regal.generator :as regal-generator]
.
Avoid single letter aliases, except for a few exceptions
Some namespaces that are very core to an application and used all over the place are better off having a shorter alias used consistently.
[datomic.api :as d]
[clojure.string :as str]
Avoid :refer :all
except in a few cases where we always use :refer :all
.
Clojure.test should always be required as [clojure.test :refer :all]
, we want that DSL to be available in every test namespace unqualified.
For cljs/cljc file you should still require clojure.test
(it will be converted to cljs.test
by the compiler under the hood), but you need to use explicit refers.
[clojure.test :refer [deftest testing is are use-fixtures]]
The other exception is a core-ext
namespace as used in Kaocha. This namespace contains functions that you could imagine going into clojure.core
, such a regex?
, exception?
,etc. This one we always load as [kaocha.core-ext :refer :all]
.
CLJC
Some particulars about dealing with cross-platform CLJC files.
Avoid splicing into maps
When you want a key/value to present in a map only for .clj or only for .cljs, then it can be tempting to do this.
;; ❌
{:foo :bar
?(:cljs [:bar :baz])}
This works, but it trips up tools based on rewrite-clj, because rewrite-clj treats this as a map with an odd number of forms. Instead do this.
;; ✔️
(merge
{:foo :bar}
?(:cljs {:bar :baz}))
Have a separate reader conditional for each top level form
;; ❌ not this
?(:clj
((defn xxx [] ...)
(defn yyy [] ...)))
;; ❌ also not this
?(:clj
(do
(defn xxx [] ...)
(defn yyy [] ...)))
;; ✔️ but this
?(:clj
(defn xxx [] ...))
?(:clj
(defn yyy [] ...))
Note that this advice is for top-level forms only. In nested forms you should do whatever makes the code more clear. Generally it's good to minimize the amount of reader conditionals.
Use a platform namespace
Abstract away differences between Clojure/JVM and ClojureScript/JavaScript by having a platform namespace with both a .clj
and .cljs
version.
;; platform.clj
(ns lambdaisland.uri.platform)
(defn string->byte-seq [String s]
(.getBytes s "UTF8"))
(defn byte-seq->string [arr]
(String. (byte-array arr) "UTF8"))
;; platform.cljs
(ns lambdaisland.uri.platform
(:require [goog.crypt :as c]))
(defn string->byte-seq [s]
(c/stringToUtf8ByteArray s))
(defn byte-seq->string [arr]
(c/utf8ByteArrayToString (apply array arr)))
This way you largely avoid using reader conditionals in your .cljc
files.The idea is also that if we use this pattern judiciously across projects, eventually we can combine those into a sort of cross-platform standard library.
Whitespace
Always have a single newline between top-level forms. If you feel the need to use multiple newlines to separate sections of a namespace, then put a comment there instead, or consider if really the namespace needs to be split.
;; ✔️ this is ok
(defn foo [])
(defn bar [])
;; ❌ this is not
(defn foo [])
(defn bar [])
;; ✔️ but you can do this
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; foo
(defn foo1 [])
(defn foo2 [])
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; bar
(defn bar [])
Destructuring
Don't get too clever with destructuring. Avoid destructuring multiple levels at once, or mixing destructuring styles in the same form.
;; ❌ can anyone still follow this?
(let [{[{:keys [foo]}] :hello} m]
,,,)
;; ✔️ do it in a few steps
(let [{[n] :hello} m
{:keys [foo]} n]
,,,)
Avoid using keywords inside a :keys vector. It works, but it's not idiomatic.
;; ❌ foo introduces a variable, i.e. it is a binding form, it should be a symbol
(let [{:keys [:foo]} m]
,,,)
;; ✔️ like this
(let [{:keys [foo]} m]
,,,)
You can still use namespaced symbols to destructure namespaced keywords, you can also namespace :keys, e.g.
(let [{:keys [foo/bar]} m]
)
(let [{:foo/keys [bar]} m]
)
;; This also allows you to tap into namespace aliases
(let [{::f/keys [bar]} m]
)