このスタイルガイドは、Bozhidar Batsov氏によるThe Clojure Style Guideの日本語翻訳版です。Toshiki Takeuchi氏による日本語翻訳版を下敷きにして作成しました。
本翻訳版のライセンスはCreative Commons Attribution 3.0 Unported Licenseとします。
本翻訳版は以下の作品を改変して作成しました。
| 著者 | タイトル | ライセンス表記 |
|---|---|---|
意訳していて、原文の意味を損なわない程度に言葉を加えたり省略している部分があります。また、訳が間違っている可能性があります。ご注意ください。なお、ガイドライン自体に対する意見はオリジナルの英語版の内容を確認の上、原文リポジトリにお願いします。
内容につきましては、原文リポジトリのコミット2bc3d2c (Sep 17, 2026)まで反映しています。
はじめに
良い手本は大切だ。
このClojureスタイルガイドは、実際のClojureプログラマーが他のClojureプログラマーに保守してもらえるコードを書くためのベストプラクティスを薦めるものだ。実際の使われ方を反映したスタイルガイドは活用されるが、支援対象である人たちから拒絶された理想に固執するスタイルガイドは、たとえそれがどれほど優れたものであったとしても全く使われないリスクがある。
本ガイドは関連するルールごとにいくつかのセクションに分かれて構成されている。各ルールにはその根拠を併記するように努めた(ただし根拠が自明だと判断した場合は省略している)。
これらのルールは、出し抜けに考えだされたものではない。その多くはスタイルガイドの編集者たちの経験、多くのClojureコミュニティメンバーからのフィードバックや提案、そして"Clojure Programming"や"The Joy of Clojure"のような高い評価を受けている様々な資料に基づいている。
このスタイルガイドの内容は、まだ確定したものではない。新たな慣習が確認されたり、Clojure自体の変更によって従来の慣習が時代遅れになったりするにつれて、このスタイルガイドは時間とともに進化する。
|
Note
|
Clojureの開発者たちはライブラリのコーディングガイドライン[1]の一覧もまとめています。これらは、あなたがいま目にしているスタイルガイドの着想の源の一つとなりました。 |
基本理念
プログラムは人が読むように書くべきで、そのついでに計算機で実行されるものである。
計算機プログラムの構造と解釈
コードは、書かれるよりも読まれることのほうがはるかに多いというのは周知の事実だ。このガイドラインは、コードの可読性を向上し、Clojureのコードに幅広い一貫性をもたらすことを目的としている。これらは適当な理想論ではなく、Clojureの実際の使用状況を反映することも意図している。既に確立された慣例と、主観的にはより優れていると思われる代替案がある場合は、確立された慣例を推奨するようにした[2]。
Clojureコミュニティ内では、特定のスタイルに関して意見が分かれている分野がいくつか存在している(例えば、セマンティック・インデントvs固定インデント、セマンティック・コメントvs統一的コメント、など)。そのような状況では一般的なスタイルはすべて受け入れられており、どれか一つを選んで一貫して適用するのはあなた次第だ。
幸いなことにClojureはLispの一つであり、Lispは根本的にシンプルである。このガイドはClojureが登場してから数年後に作成されたが(初版の公開は2013年の初頭)、実際に使われていたClojureコードのほとんどはかなり均一だった。この理由としては、先に述べたようにLispが持つシンプルさに加え、Clojureのコミュニティが最初からCommon LispやSchemeといった既存のLisp方言のスタイル慣習の多くを取り入れてきたという事実を挙げておく。そのおかげで、本ガイドの作成は、特にCommunity Ruby Style Guideのような大変な苦痛を伴った作業に比べるとかなり簡単でスムーズに行えた[3]。
Clojureはシンプルさと明快さでよく知られている。このガイドが、最大限のシンプルさと明快さを実現する手助けになれば幸いだ。
一貫性に関する注意事項
愚かな一貫性は狭量な精神の厄介者であり、二流の政治家、哲学者、神学者に崇拝されている。
スタイルガイドは一貫性に関するものである[4]。このスタイルガイドに準拠することは重要である。プロジェクト内の一貫性はそれ以上に重要である。1つのクラスやメソッド内の一貫性は最も重要である。
しかしながら、ときにはスタイルガイドの推奨事項が当てはまらないこともあり、一貫性を保つべきではない状況を見極めることも大切である。迷ったときは、あなたの判断に従うこと。他の事例を参考にし、一番良いと思える方法を選ぶようにする。そして、遠慮なく質問すること!
とりわけ、このガイドに準拠するためだけに後方互換性を損なうようなことはしてはいけない!
特定のガイドラインを無視できる、いくつかの正当な理由がある:
-
ガイドラインを適用することで、このスタイルガイドに従ったコードを読むことに慣れている者であってもコードの可読性を損なう場合。
-
おそらく歴史的な理由からガイドラインに従っていない、周囲のコードとの間で整合性を保つため。とはいえ、これは(真のエクストリーム・プログラミング スタイルによって)誰かがやらかしたことの尻拭いをする機会でもある。
-
当該のコードはガイドラインよりも以前から存在しており、それを変更すべき他の理由もないため。
-
スタイルガイドが推奨している機能をサポートしていない、古いバージョンのClojureとの互換性を維持する必要がある場合。
ソースコードのレイアウトと構造
ほとんどすべての人は、自分のスタイル以外のあらゆるスタイルは汚くて読みにくい、と思っている。「自分のスタイル以外の」を削れば、それはおそらく正しい…
1行の最大長
可能なら、1行が80文字を超えないようにする。
タブvsスペース
インデントにはスペースを使う。タブは使わない。
ボディのインデント
ボディパラメーターをもつフォームのボディのインデントには2つのスペースを使う。これにはすべてのdefフォーム、ローカル束縛をもつスペシャルフォームおよびマクロ(例:loop, let, when-let)、そしてwhen, cond, as->, cond->, case, with-*などの多くのマクロが含まれる。
;; 良い
(when something
(something-else))
(with-out-str
(println "Hello, ")
(println "world!"))
;; 悪い - 4つのスペース
(when something
(something-else))
;; 悪い - 1つのスペース
(with-out-str
(println "Hello, ")
(println "world!"))
関数の引数の揃え方
複数行にわたる関数(マクロ)の引数は左揃えにする。
;; 良い
(filter even?
(range 1 10))
;; 悪い - 引数が関数名と揃っている(1つのスペースによるインデント)
(filter even?
(range 1 10))
;; 悪い - 2つのスペースによるインデント
(filter even?
(range 1 10))
このガイドラインの根拠はごく単純である。引数が目立つようにまとめて並んで書かれていれば、人間の脳にとって処理しやすいからだ。
関数の引数のインデント
|
Note
|
原則として、利用可能な横幅に制限がある場合を除き、前述のガイドラインで示された書式に従うべきです。 |
関数(マクロ)名と同じ行に引数を書かない場合は、引数のインデントには1つのスペースを使う。
;; 良い
(filter
even?
(range 1 10))
(or
ala
bala
portokala)
;; 悪い - 2つのスペースによるインデント
(filter
even?
(range 1 10))
(or
ala
bala
portokala)
Lispに馴染みがない人には奇妙で特別なルールに見えるが、その背景にある理由はとても単純だ。関数呼び出しは単なる通常のリストリテラルに過ぎず、複数行にわたる場合は他のコレクション型リテラルと同じように整列するのが一般的だ:
;; リストリテラル
(1
2
3)
;; ベクターリテラル
[1
2
3]
;; セットリテラル
#{1
2
3}
確かに、Clojureにおいてリストリテラルはあまり一般的ではない。そのため、多くの人にとってリストとは単なる呼び出しの構文に過ぎないと理解するのも無理からぬことである。
副次的な利点として、このケースでも関数の引数は整列された状態になっている。とはいえ、たまたま関数名と揃って並んでいるだけだ。
束縛の揃え方
let(およびlet系)の束縛を左揃えにする。
;; 良い
(let [thing1 "some stuff"
thing2 "other stuff"]
(foo thing1 thing2))
;; 悪い
(let [thing1 "some stuff"
thing2 "other stuff"]
(foo thing1 thing2))
マップのキーの揃え方
マップのキーを左揃えにする。
;; 良い
{:thing1 thing1
:thing2 thing2}
;; 悪い
{:thing1 thing1
:thing2 thing2}
;; 悪い
{:thing1 thing1
:thing2 thing2}
改行コード
Unixスタイルの改行コード(LF)を使用する[5]。
|
Tip
|
Windows(WSL/WSL2を除く)上でGitを使っているなら、次の設定を追加してリポジトリにWindowsの改行コード(CRLF)の混入を防ぐのもいい。
|
括弧のスペース
開き括弧((, {, [)の前や、閉じ括弧(), }, ])の後ろにテキストがある場合は、括弧とテキストとの間にスペースを1つ設ける。逆に、開き括弧とそれに続くテキスト、閉じ括弧と直前のテキストの間にはスペースを入れない。
;; 良い
(foo (bar baz) quux)
;; 悪い
(foo(bar baz)quux)
(foo ( bar baz ) quux)
シーケンシャルコレクションのリテラルにコンマを使わない
構文糖衣はセミコロンのガンを引き起こす。
シーケンシャルコレクションのリテラルの要素の間にコンマを使わない。
;; 良い
[1 2 3]
(1 2 3)
;; 悪い
[1, 2, 3]
(1, 2, 3)
マップリテラルのコンマ
コンマや改行を使い、マップリテラルの可読性を向上させることを検討する。
;; 良い
{: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)
)
このルールの例外として、REPL指向プログラミングのリッチコメントがある。コメントフォームの閉じ括弧も一緒にまとめてしまうと、コメント内の最後のフォームが使いにくくなってしまうのを避けるためだ。リッチコメントの閉じ括弧は、次の行に記述しても構わない。
;; 良い。リッチコメントの最後の閉じ括弧は次の行に記述。
(comment
(range 5)
(interpose '* (range 5))
(->> (range 5) (interpose '*))
(= *1 *2)
)
トップレベルのフォーム間の空行
トップレベルのフォームの間には1行の空行を挟む。
;; 良い
(def x ...)
(defn foo ...)
;; 悪い
(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などで見られるようなペアで構成される要素が同じ行に収まらない場合に、グループ化する目的で空行を入れるのは例外とする。
;; 良い
(defn fibo-iter
([n] (fibo-iter 0 1 n))
([curr nxt n]
(cond
(zero? n) curr
:else (recur nxt (+' curr nxt) (dec n)))))
;; まあ大丈夫 - 空行は cond のペアを分けている
(defn fibo-iter
([n] (fibo-iter 0 1 n))
([curr nxt n]
(cond
(zero? n)
curr
:else
(recur nxt (+' curr nxt) (dec n)))))
;; 悪い
(defn fibo-iter
([n] (fibo-iter 0 1 n))
([curr nxt n]
(cond
(zero? n) curr
:else (recur nxt (+' curr nxt) (dec n)))))
長い関数の定義内に空行をところどころに入れるのは良い考えのように思うことがあるかもしれないい。そのような状況になったら、その関数に処理を詰め込みすぎていないか、関数を分割できるのではないかも検討すべきだ。
行末の空白を避ける
行末の空白を避ける。
1名前空間に1ファイル
1つのファイルでは1つの名前空間のみを用い、1つの名前空間は1つのファイルのみで用いる。
;; 良い
(ns foo.bar)
;; 悪い
(ns foo.bar)
(ns baz.qux)
;; 悪い
(in-ns quux.quuz)
(in-ns quuz.corge)
;; 悪い
;; (ns foo.bar) や (in-ns foo.bar) を複数のファイル内で用いる
名前空間の宣言
単一セグメントの名前空間を使わない
単一セグメントから成る名前空間を使わない。
;; 良い
(ns example.ns)
;; 悪い
(ns example)
名前空間は名前の曖昧さを解消するためのものだ。名前空間に単一セグメントを使うと、同じように単一セグメントを名前空間に使っている他の人たちと直に競合することになり、すなわち他のコードベースと衝突する可能性が高くなる。
実際のところ、この規則は他のライブラリとの間で名前空間の競合を避けるため、ライブラリでは単一セグメントの名前空間を絶対に使ってはならないということだ。もちろん、あなたのプライベートなアプリケーションの中であれば、好きなようにして構わない。
|
Tip
|
名前空間を1つ持つライブラリでは、domain.library-nameやlibrary-name.coreといった名前を使うのが一般的だ。名前空間の命名に関するトピックについては、このまま読み進み進んでほしい。
|
単一セグメントの名前空間を避けるべき理由は他にもあり、それらを使おうとする前によくよく検討すべきだ。
名前空間セグメントの制限
例えば5セグメントを超えるような、無駄に長い名前空間を使わない。
必要な参照を指定したnsフォーム
どの名前空間もnsフォームで開始し、フォーム内にrefer, require, importの指定を必要に応じて含める。各指定は、慣例としてこの順序で記述する。
(ns examples.ns
(:refer-clojure :exclude [next replace remove])
(:require [clojure.string :as s :refer [blank?]])
(:import java.util.Date))
ns中の改行
依存対象が複数ある場合、それぞれを独立した行に記述すると良い。そうすることでソートが容易になり、読みやすく、変更したときの差分表示も見やすくなる。
;; より良い
(ns examples.ns
(: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 examples.ns
(: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 examples.ns
(: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]))
:useよりも:requireが好ましい
nsフォームでは:require :refer :allよりも:require :refer、それよりも:require :asが好ましい。また:useよりも: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))
requireとimportのソート
nsフォームではrequireとimportの対象をソートする。特にrequire/importする名前空間が非常に多い場合には、ソートすることで可読性が向上し、重複を避けられるようになる。
;; 良い
(ns examples.ns
(:require
[baz.core :as baz]
[clojure.java.shell :as sh]
[clojure.set :as set]
[clojure.string :as s :refer [blank?]]
[foo.bar :as foo]))
;; 悪い
(ns examples.ns
(:require
[clojure.string :as s :refer [blank?]]
[clojure.set :as set]
[baz.core :as baz]
[foo.bar :as foo]
[clojure.java.shell :as sh]))
慣用的な名前空間エイリアスを使う
Clojureの主要な名前空間の多くは、プロジェクト内で使用が推奨される慣用的なエイリアス(別名)がある。例えば、clojure.stringを読み込む最も一般的な方法は[clojure.string :as str]だ。
|
Note
|
clojure.core.strを隠蔽しているように見えますが、実際はそうではありません。同じ名前空間内でclojure.core/strとclojure.string/*は、strとstr/whateverとして衝突することなく使えます。
|
;; 良い
(ns ... (:require [clojure.string :as str] ...)
(str/join ...)
;; あまり良くない - 慣用的に `str/` を使うべきだ
(ns ... (:require [clojure.string :as string] ...)
(string/join ...)
次のセクションで述べるように、一般的には一意性が確保できるなら名前空間の末尾のセグメントをエイリアスとして使用するのが慣例となっている。1つのセグメントで確保できないなら2つ使い、通常はcljやcoreといった冗長な部分を削除する。
ClojureのコアおよびContrib名前空間のうち、以下の名前空間は慣用的なエイリアスが存在する:
| 名前空間 | 慣用的なエイリアス |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
それから、いくつかのコアおよびContirb名前空間では、より短いエイリアスが使われている:
| 名前空間 | 慣用的なエイリアス |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
一般的に使われているコミュニティライブラリの中にも、広く使われている慣用的なエイリアスを持つ名前空間が数多くある:
| 名前空間 | 慣用的なエイリアス |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
Note
|
上記のリストのうち、コミュニティライブラリの公式ドキュメントに記載されているエイリアスとは異なるものがあります。(訳者) |
良い名前空間エイリアスのためのレシピ
上記で、よく使われる名前空間と慣用的なエイリアスをいくつか紹介した。しかし、それらは少しばかり一貫性に欠けている。
-
clojure.stringがstrに -
clojure.pprintがppに -
clojure.walkがwalkに -
clojure.spec.alphaがsに
これらに共通するのは簡潔さを目指しつつも、エイリアスにある程度の意味を持たせようとしていることは明白である。(clojure.walkをwにエイリアスするのは簡潔ではあるが、ほとんど意味が残っていない。)
しかし、慣用的なエイリアスを持たない他の名前空間についてはどうすればよいのだろうか? それらのエイリアスを導き出すアプローチには一貫性を持たせる必要がある。そうでないとClojureの共有コードベースで作業する他の人たちは大きな混乱に陥るだろう。下記に従うべきいくつかのルールを示す[6]。
-
エイリアスは、名前空間の先頭部分を取り除いたものと同じにする。
(ns com.example.application (:require [clojure.java.io :as io] [clojure.reflect :as reflect])) -
各エイリアスがそれぞれ一意になるのに必要なだけ末尾の部分を残す。
[clojure.data.xml :as data.xml] [clojure.xml :as xml]Tip名前空間のエイリアスにはドットを含めることができる。活用しよう。 -
エイリアス内のcoreやcljといった冗長な単語を取り除く。
[clj-time.core :as time] [clj-time.format :as time.format]
一貫した名前空間エイリアスの使用
プロジェクト全体を通して、名前空間エイリアスの一貫性を保つことは良いことだ。例えば、ある名前空間でclojure.stringをstrとして読み込む一方で、別の名前空間ではstirngとして読み込むようなことをしてはならない。前述の2つのガイドラインに従えば基本的には問題はないだろうが、名前空間エイリアスに独自の命名体系を採用する場合はプロジェクト内で一貫して適用することが依然として重要だ。
命名規則
プログラミングで本当に難しいのは、キャッシュの無効化と命名の仕方だけだ。
名前空間の命名方法
名前空間に名前を付ける際は、以下の2つの形式のいずれかを採用することが望ましい。
-
project.module -
organization.project.module
project.module方式の名前空間を使う場合、1つの名前空間の実装だけを持つプロジェクトならば一般的な名前はproject.coreである。それ以外のあらゆるケースにおいてはproject.coreを名前に使うのは避けるべきで、より意味が伝わる名前をつけるほうが常に望ましい。
名前空間セグメントにおける複数単語
複数単語からなる名前空間セグメントにはlisp-caseを使う(例:bruce.project-euler)。
|
Note
|
Lisp以外の多くのプログラミングコミュニティではlisp-caseをkebab-caseと呼んでいますが、kebab-caseの呼称が作り出されるよりずっと前からLispが存在していたことは周知の事実です。
|
関数、変数
関数名や変数名にはlisp-caseを使う。
;; 良い
(def some-var ...)
(defn some-fun ...)
;; 悪い
(def someVar ...)
(defn somefun ...)
(def some_fun ...)
プロトコル、レコード、構造体、型
プロトコル、レコード、構造体、型にはCapitalCaseを用いる。(HTTP, RFC, XMLのような頭字語は大文字を保持する。)
|
Note
|
CapitalCaseはUpperCamelCase, CapitalWords, PascalCaseとしても知られています。
|
;; 良い
(defprotocol Serializable ...)
(defrecord HttpRequest ...)
(deftype XMLParser ...)
;; 悪い
(defprotocol serializable ...)
(defrecord http-request ...)
(deftype xml_parser ...)
述語
述語(ブール値を返す関数)の名前はクエスチョンマーク?で終わるべきだ(例:even?)。
;; 良い
(defn palindrome? ...)
;; 悪い
(defn palindrome-p ...) ; Common Lisp スタイル
(defn is-palindrome ...) ; Java スタイル
安全でない関数
STMトランザクション内で安全ではない関数・マクロの名前は、エクスクラメーションマーク!で終わるべきだ(例:reset!)。! は呼び出し側に対して、その関数が副作用を持っていてdosyncブロック内で呼ぶべきでないことを伝える。
;; 良い
(defn save-user! [user]
(db/insert! user))
;; 悪い - 副作用を持つ関数の名前が ! で終わっていない
(defn save-user [user]
(db/insert! user))
DynamicなVar
bindingによる再束縛を想定している(つまり動的である)場合は、名前の両端に*を付ける。これは見た目からearmuffs(耳あて)と呼ばれている。
;; 良い
(def ^:dynamic *a* 10)
;; 悪い
(def ^:dynamic a 10)
定数
定数のために特別な表記をしない。特に指定しない限り、すべて定数とみなす。
;; 良い
(def max-size 10)
;; 悪い
(def MAX-SIZE 10) ; Java スタイル
(def +max-size+ 10) ; Common Lisp スタイル、グローバル定数
(def *max-size* 10) ; Common Lisp スタイル、グローバル変数
|
Note
|
*clojure-version* がこの慣例に反しているのはよく知られていますが、この命名は歴史的な例外であり模範とするべきものではないです。
|
使用しない束縛
分配束縛の対象や仮引数の名前のうち、その値が当該のコードで使用されないものについてはアンダースコア_を使う。
;; 良い
(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!"))
しかしながら、コードの理解を助ける場合は、使用しない引数や分配束縛のマップに明示的な名前を付けておくと便利である。その際は名前の先頭にアンダースコアを付加して、その変数が実際には使われないことを明確にする。
;; 良い
(defn myfun1 [context _]
(assoc context :foo "bar"))
(defn myfun2 [context {:keys [id]}]
(assoc context :user-id id))
;; より良い
(defn myfun1 [context _user]
(assoc context :foo "bar"))
(defn myfun2 [context {:keys [id] :as _user}]
(assoc context :user-id id))
慣用名
predやcollのような慣用名はclojure.coreの例が参考になる。
-
関数内では、
-
f,g,h- 関数入力 -
n- サイズを示す整数値 -
index,i- 整数のインデックス -
x,y- 数値 -
xs- シーケンス -
m- マップ -
k,ks- キー、複数のキー -
v,vs- 値, 複数の値(キーと値のペアにおける「値」) -
s- 文字列入力 -
re- 正規表現 -
sym- シンボル -
coll- コレクション -
pred- 述語クロージャ -
& more- 可変長引数 -
xf- xform(トランスデューサー) -
ns- 名前空間[7]
-
-
マクロ内では、
-
expr- 式 -
body- マクロ本体 -
binding- マクロの束縛ベクター
-
-
defprotocol,deftype,defrecord,reifyなどの中で定義されるメソッド内では、-
this- 第1引数として、オブジェクトへの参照、あるいはそのオブジェクトを表す一貫性のある名前
-
関数
関数名の後ろの任意の改行
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))
1行の関数
引数ベクターと短い関数本体との間の改行は省略できる。
;; 良い
(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個の引数を持つものが関数の振る舞いを定義していて、N個(< K)の引数を持つアリティはK引数のアリティの部分適用、N個(> K)の引数を持つアリティは可変長引数であるK引数のアリティの畳み込み、という場合がある。
;; 良い - n番目のアリティを見つけやすい
(defn foo
"I have two arities."
([x]
(foo x 1))
([x y]
(+ x y)))
;; まあ大丈夫 - 他のアリティは2引数のアリティの応用
(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)))
関数の長さ
関数はLOC (lines of code)が10行を超えないようにする。理想的には、ほとんどの関数はLOCが5行より短いほうが良い。短い関数は理解やテストが容易で、組み合わせやすい。
関数の位置パラメーターの制限
3つか4つを超える位置パラメーターを持つパラメーターリストの使用を避ける。長いパラメーターリストは覚えにくく、呼び出し側で間違えやすくなる。代わりにマップを使うほうが好ましい。
;; 良い
(defn create-user [{:keys [name email role admin?]}]
...)
;; 悪い - 呼び出し側は正確な順序を覚えていなければならない
(defn create-user [name email role admin?]
...)
事前/事後条件
関数の本体内でチェックを行うより、関数の事前(pre)/事後(post)条件によるチェックのほうが好ましい。
;; 良い
(defn foo [x]
{:pre [(pos? x)]}
(bar x))
;; 悪い
(defn foo [x]
(if (pos? x)
(bar x)
(throw (IllegalArgumentException. "x must be a positive number!")))
慣用的な構文
動的な名前空間の操作
requireやreferのような、動的に名前空間を操作する関数の使用を避ける。これらはREPL環境以外では全く必要がないものだ。
;; 良い - ns フォームを使う
(ns my-app.core
(:require [clojure.string :as str]))
;; 悪い - ソースファイル内で require を関数として呼んでいる
(require '[clojure.string :as str])
前方参照
前方参照を避ける。前方参照は時として必要になるが、実際にはそのような機会はまれだ。前方参照はコードをトップダウンで追いかけるのを難しくし、循環した依存関係を隠してしまう可能性がある。
declare
前方参照が必要なときは、declareを使って前方参照を可能にする。
;; 良い - 本当に前方参照が必要なら declare を使う
(declare process-children)
(defn process-node [node]
(when (:children node)
(process-children node)))
(defn process-children [node]
(mapv process-node (:children node)))
高階関数
loop/recurよりも、mapのような高階関数のほうが好ましい。高階関数はより宣言的で、どのように反復処理を行うかではなく何をしたいのかを表現するものであり、シーケンスライブラリの他の機能とも自然に組み合わせることができる。
;; 良い
(map inc [1 2 3 4 5])
;; 悪い - 同じことを行うために、わざわざループを使っている
(loop [xs [1 2 3 4 5] result []]
(if (empty? xs)
result
(recur (rest xs) (conj result (inc (first xs))))))
clojure.coreに属する名前の隠蔽
ローカル束縛でclojure.coreに属する名前を隠さない。
;; 悪い - 関数内では clojure.core/map を完全修飾しなければならなくなる
(defn foo [map]
...)
varの変更
varの値を変更するには、defの代わりにalter-var-rootを使う。
;; 良い
(def thing 1) ; thingの値は1
; thingを用いた何らかの処理
(alter-var-root #'thing (constantly nil)) ; thingの値はnil
;; 悪い
(def thing 1)
; thingを用いた何らかの処理
(def thing nil)
; thingの値はnil
nil punning
シーケンスが空かどうかをチェックするには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))))
シーケンスからベクターへの変換
シーケンスをベクターに変換する必要があるときは、intoよりもvecを用いるほうが好ましい。
;; 良い
(vec some-seq)
;; 悪い
(into [] some-seq)
ブール値への変換
何かを実際のブール値(trueまたはfalse)に変換する必要があるときはboolean関数を使う。
;; 良い
(boolean (foo bar))
;; 悪い
(if (foo bar) true false)
|
Note
|
Clojureにおいて「偽」とみなされる値はfalseとnilだけであることを忘れないように。その他の値をboolean関数に渡すと「真」であると評価します。
|
Clojureでは実際のブール値が必要になることはほとんどないが、必要になったときに取得方法を知っていると便利だ。
when vs if
(if condition (something…))や(if … (do …))のように真の場合の処理しか行わない場合は、ifではなくwhenを使う。
;; 良い
(when pred
(foo)
(bar))
;; 悪い
(if pred
(do
(foo)
(bar)))
if-let
let + ifの代わりにif-letを使う。
;; 良い
(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を使う。
;; 良い
(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)))
when-not
(when (not …) …) の代わりに when-not を使う。
;; 良い
(when-not pred
(foo)
(bar))
;; 悪い
(when (not pred)
(foo)
(bar))
when-not vs if-not
(if-not … (do …))の代わりにwhen-notを使う。
;; 良い
(when-not pred
(foo)
(bar))
;; 悪い
(if-not pred
(do
(foo)
(bar)))
printf
(print (format …))よりもprintfが好ましい。
;; 良い
(printf "Hello, %s!\n" name)
;; 大丈夫
(println (format "Hello, %s!" name))
柔軟な比較関数
比較を行うときは、Clojure関数の<や>などは可変長引数を受け入れるという事実を活用しよう。
;; 良い
(< 5 x 10)
;; 悪い
(and (> x 5) (< x 10))
無意味な無名関数を使用しない
必要がないなら無名関数でラップしない。
;; 良い
(filter even? (range 1 10))
;; 悪い
(filter #(even? %) (range 1 10))
複数フォームの関数リテラルを使用しない
関数本体が2つ以上のフォームを含む場合は、関数リテラルを使用しない。
;; 良い
(fn [x]
(println x)
(* x 2))
;; 悪い (doフォームを明示的に使わなければならない)
#(do (println %)
(* % 2))
無名関数 vs complement, comp, partial
complement, comp, partialよりも無名関数を使うほうが好ましい。ほとんどの場合、そうすることでコードが単純になる[8]。
complement
;; 良い
(filter #(not (some-pred? %)) coll)
;; まあまあ
(filter (complement some-pred?) coll)
comp
;; `(:require [clojure.string :as str])` を前提に...
;; 良い
(map #(str/capitalize (str/trim %)) ["top " " test "])
;; まあまあ
(map (comp str/capitalize str/trim) ["top " " test "])
とはいえ、トランスデューサーのチェーンを構築する際にcompは非常に便利だ。
;; 良い
(def xf
(comp
(filter odd?)
(map inc)
(take 5)))
partial
;; 良い
(map #(+ 5 %) (range 1 10))
;; まあまあ
(map (partial + 5) (range 1 10))
スレッディングマクロ
深いネストよりも、スレッディングマクロ->(thread-first)と->>(thread-last)の使用が好ましい。
;; 良い
(-> [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)))
スレッディングマクロと省略可能な括弧
引数が指定されていない関数に対してスレッディングマクロを使う場合、括弧は不要だ。必要な場合のみ括弧を使うこと。
;; 良い
(-> x fizz :foo first frob)
;; 悪い。括弧は見づらくなるだけで不要だ。
(-> x (fizz) (:foo) (first) (frob))
;; 良い。引数がある場合に括弧は必要だ。
(-> x
(fizz a b)
:foo
first
(frob x y))
スレッディングマクロの揃え方
スレッディングマクロ->(thread-first)と->>(thread-last)の引数は揃える。
;; 良い
(->> (range)
(filter even?)
(take 5))
;; 悪い
(->> (range)
(filter even?)
(take 5))
cond のデフォルト条件
condにおいて、すべての場合に当てはまるテスト式として:elseを使う。
;; 良い
(cond
(neg? n) "negative"
(pos? n) "positive"
:else "zero")
;; 悪い
(cond
(neg? n) "negative"
(pos? n) "positive"
true "zero")
condp vs cond
述語と式が変わらない場合、condよりもcondpのほうが好ましい。
;; 良い
(cond
(= x 10) :ten
(= x 20) :twenty
(= x 30) :thirty
:else :dunno)
;; ずっと良い
(condp = x
10 :ten
20 :twenty
30 :thirty
:dunno)
case vs cond/condp
テスト式がコンパイル時に定数の場合、condやcondpの代わりにcaseを使うのが好ましい。
;; 良い
(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などの中では、短いフォームを用いる。それが無理なら、コメントや空行を使用してペアグループを見えやすくする。
;; 良い
(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)))
述語としてのセット
適切な場合は、セットを述語として使う。
;; 良い
(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とdec
(+ x 1)や(- x 1)の代わりに(inc x)や(dec x)を使う。
pos?とneg?
(> x 0), (< x 0), (= x 0)の代わりに(pos? x), (neg? x), (zero? x)を使う。
list* vs cons
ネストされたcons呼び出しの代わりにlist*を使う。
;; 良い
(list* 1 2 3 [4 5])
;; 悪い
(cons 1 (cons 2 (cons 3 [4 5])))
糖衣されたJava呼び出し
糖衣されたJava呼び出しフォームを用いる。
;;; オブジェクト生成
;; 良い
(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フラグには簡易メタデータ表記
キーがキーワード、値がブール値trueのスロットしか持たないメタデータには、簡易メタデータ表記を使う。
;; 良い
(def ^:private a 5)
;; 悪い
(def ^{:private true} a 5)
プライベート
コード中のプライベート部分には印を付ける。
;; 良い
(defn- private-fun [] ...)
(def ^:private private-var ...)
;; 悪い
(defn private-fun [] ...) ; 全くプライベートでない
(defn ^:private private-fun [] ...) ; 冗長な記述だ
(def private-var ...) ; 全くプライベートでない
プライベートなvarへのアクセス
例えばテストのためにプライベートなvarにアクセスするには、@#'some.ns/varフォームを使う。
メタデータ付加は慎重に
メタデータを何に付加するかについては、よく注意したほうが良い。
;; `a` で参照されるvarにメタデータを付加している
(def ^:private a {})
(meta a) ;=> nil
(meta #'a) ;=> {:private true}
;; 空のハッシュマップ値にメタデータを付加している
(def a ^:private {})
(meta a) ;=> {:private true}
(meta #'a) ;=> nil
データ構造
10種のデータ構造を処理できる機能を10個用意するより、1種のデータ構造を処理できる機能を100個用意するほうが良い。
リストを避ける
汎用的なデータ置き場としてリストを使うことを避ける(リストが本当に必要な場合を除く)。ベクターは定数時間O(1)のインデックスによるアクセスと、conjによる末尾での効率的な要素追加を提供し、他の標準ライブラリともうまく連携して機能する。
;; 良い
[1 2 3]
;; 悪い - 単なる格納容器として使われるリスト
'(1 2 3)
マップのキーにはキーワードを用いる
マップのキーにはキーワードを用いるほうが好ましい。キーワードはインターンされて高速な等価性チェックが可能で、キーワードを関数として使うことでマップを検索でき、値とは見た目が際立って異なっている。
;; 良い
{:name "Bruce" :age 30}
;; 悪い
{"name" "Bruce" "age" 30}
コレクションのリテラル構文
可能なら、コレクションのリテラル構文を用いるほうが好ましい。ただしセットを定義するときは、値がコンパイル時に定数になる場合のみリテラル構文を使用する。
;; 良い
[1 2 3]
#{1 2 3}
(hash-set (func1) (func2)) ; 実行時に決定する値
;; 悪い
(vector 1 2 3)
(hash-set 1 2 3)
#{(func1) (func2)} ; もし (func1) = (func2) だったら実行時例外が投げられる
コレクションにインデックスでアクセスすることを避ける
できる限り、コレクションの要素にインデックスでアクセスすることを避ける。分配束縛とシーケンス関数は意図を明確にし、コレクションのオーダーが変わっても正常に動作する。
;; 良い - 分配束縛は意図が明確
(let [[x y] point]
(process x y))
;; 悪い - 不安定で、インデックス0と1の要素が何なのか不明
(process (nth point 0) (nth point 1))
マップから値を取得する関数としてのキーワード
可能なら、キーワードをマップから値を取得する関数として用いるのが好ましい。
(def m {:name "Bruce" :age 30})
;; 良い
(:name m)
;; 必要以上の冗長な記述だ
(get m :name)
;; 悪い - NullPointerException の影響を受けやすい
(m :name)
関数としてのコレクション
ほとんどのコレクションはその要素の関数であることを活用する。
;; 良い
(filter #{\a \e \o \i \u} "this is a test")
;; 悪い - 汚すぎて書けない
一時的コレクションを避ける
パフォーマンス問題がクリティカルとなる部分を除いて、一時的(transient)コレクションの使用を避ける。一時的コレクションは複雑さが増加する(スレッド間で共有できず、値の変更には別の関数が必要)。一方で、永続的コレクションはほとんどのユースケースにおいて既にかなり高速である。
;; 良い - 通常のコードは永続的コレクションで十分
(reduce conj [] (range 1000))
;; ベンチマークの結果が一時的コレクションの使用を正当化するホットパスでの使用なら大丈夫
(persistent! (reduce conj! (transient []) (range 1000)))
Javaのコレクションを避ける
Javaのコレクションの使用を避ける。Javaのコレクションは可変でClojureのシーケンス抽象化を実装しておらず、標準ライブラリとの間に互換性がない。
;; 良い
{:name "Bruce" :age 30}
;; 悪い
(doto (java.util.HashMap.)
(.put :name "Bruce")
(.put :age 30))
Javaの配列を避ける
Java呼び出しや、プリミティブ型を多用するパフォーマンスクリティカルなコードを除いて、Javaの配列は使用を避ける。Javaの配列は可変でリテラル構文がなく、読みやすい形式で表示されない。
;; 良い
[1 2 3]
;; 悪い - Javaとの相互運用性やプリミティブ型の性能が必要でない限り使わない
(int-array [1 2 3])
タイプとレコード
レコードのコンストラクタ
タイプやレコードのインスタンスを作るのにJava呼び出しを用いない。deftypeやdefrecordが自動的に生成したコンストラクタ関数を使用する。そうすることで、deftypeやdefrecordを利用していることが明確になる。詳しくはこの記事を参照のこと。
(defrecord Foo [a b])
(deftype Bar [a b])
;; 良い
(->Foo 1 2)
(map->Foo {:b 4 :a 3})
(->Bar 1 2)
;; 悪い
(Foo. 1 2)
(Bar. 1 2)
deftypeはmap->Typeというコンストラクタを作らないことに注意する。レコードでのみ使用できる。
カスタムレコードコンストラクタ
例えばレコード生成時にプロパティのバリデーションを行うなど、必要に応じてタイプ/レコードの自動生成されたコンストラクタ関数をラップする形で独自のコンストラクタ関数を追加する。詳しくはこの記事を参照のこと。
(defrecord Customer [id name phone email])
(defn make-customer
"Creates a new customer record."
[{:keys [name phone email]}]
{:pre [(string? name)
(valid-phone? phone)
(valid-email? email)]}
(->Customer (next-id) name phone email))
このようなカスタムコンストラクタには、好きな命名規則や構造を用いて構わない。
カスタムレコードコンストラクタの命名
自動生成されたタイプ/レコードのコンストラクタ関数を同じ名前で上書きしない。これらのコンストラクタ関数は一定の振る舞いをすると期待されているため、その挙動を変更することは驚き最小の原則に反する。詳しくはこの記事を参照のこと。
(defrecord Foo [num])
;; 良い
(defn make-foo
[num]
{:pre [(pos? num)]}
(->Foo num))
;; 悪い
(defn ->Foo
[num]
{:pre [(pos? num)]}
(Foo. num))
状態の変更
トランザクションと参照
io!マクロ
トランザクションの中で思いがけずI/Oコールを呼んでしまったときの問題を回避するため、すべてのI/Oコールをio!マクロでラップすることを検討する。io!マクロはトランザクションの中から呼ばれた場合は例外を投げるので、意図しない副作用の発生(リトライによりおそらく複数回)を明確に阻止できる。
;; 良い - I/Oコールはトランザクションの中で意図せず使われないようにガードされる
(defn save-to-file [path content]
(io! (spit path content)))
;; 悪い - dosync の中で関数が呼ばれた場合、密かに複数回実行される可能性がある
(defn save-to-file [path content]
(spit path content))
ref-setを避ける
できる限りref-setは使用しない。
(def r (ref 0))
;; 良い
(dosync (alter r + 5))
;; 悪い
(dosync (ref-set r 5))
小さいトランザクション
トランザクションのサイズ(包んでいる処理の量)をできる限り小さく保つようにする。大きいトランザクションは他のトランザクションと競合する可能性が高くなり、リトライの発生と性能低下につながる。
;; 良い - トランザクションは協調アップデートのみカバーしている
(let [data (fetch-data source)]
(dosync (alter results conj data)))
;; 悪い - トランザクション内に遅いI/Oがあると(トランザクションが)オープンしたままになる
(dosync
(let [data (fetch-data source)]
(alter results conj data)))
同一Refに対する長短期トランザクションの混在を避ける
同一のRefに作用する、短期のトランザクションと長期のトランザクションの両方を持つことを避ける。短期のトランザクションが変更をコミットすると、同じRefにアクセスしている長期のトランザクションは最初から処理のやり直しになってしまい、結果として長期のトランザクションの実行機会を奪ってしまう。
エージェント
エージェントのsend
処理がCPUバウンドで、かつI/Oや他スレッドをブロックしないときだけsendを用いる。sendは固定サイズのスレッドプールにアクションを投入するため、ブロッキングする処理がアクションにあると他のエージェントの処理が滞ることになる。
;; 良い - 純粋な計算, ブロッキングしない
(send agent-a + 42)
エージェントのsend-off
処理がスレッドをブロック、スリープさせたり、そうでなくても停滞させるかもしれない場合はsend-offを用いる。send-offは可変サイズのスレッドプールにアクションを投入するため、ブロッキングする処理がアクションにあっても他のエージェントの処理が滞ることはない。
;; 良い - ブロッキングが発生する可能性があるI/Oには send-off を使う
(send-off agent-b (fn [state] (assoc state :data (slurp url))))
;; 悪い - send で送られたブロッキングするI/Oは、固定サイズのスレッドプールを使い果たしてしまう可能性がある
(send agent-b (fn [state] (assoc state :data (slurp url))))
アトム
トランザクション内で更新しない
STMトランザクションの中でアトムを更新することを避ける。アトムはSTMによる調整の対象ではないため、dosyncによるトランザクション内でのアトムに対するswap!での更新は、最終的に成功したコミットのときだけでなくリトライが起きるたびに実行される。
;; 良い - アトムの更新はトランザクション処理の後に実施している
(dosync (alter account-a - 100)
(alter account-b + 100))
(swap! transfer-log conj {:from :a :to :b :amount 100})
;; 悪い - トランザクションがリトライされるごとにログエントリが追加されてしまう
(dosync (alter account-a - 100)
(alter account-b + 100)
(swap! transfer-log conj {:from :a :to :b :amount 100}))
reset!よりもswap!が好ましい
可能なら、reset!よりもswap!を使うようにする。
(def a (atom 0))
;; 良い
(swap! a + 5)
;; あまり良くない
(reset! a 5)
数学
Java呼び出しよりもclojure.mathの関数
Java呼び出しや独自実装よりも、clojure.mathの数学関数を使うほうが好ましい。
;; 良い
(clojure.math/pow 2 5)
;; まあまあ
(Math/pow 2 5)
JDKのパッケージjava.lang.Mathは多くの便利な数学関数へのアクセスを提供している。Clojure 1.11より前は、これらの利用にはJavaとの相互運用性に頼っていたが、それには関数の見つけやすさ、プリミティブ型のパフォーマンス、高階適用、移植性といった面で課題があった。新しいclojure.math名前空間はjava.lang.Mathで利用可能なメソッドに対してラッパー関数を提供し、longおよびdouble型のオーバーロードに対応したプリミティブ型での高速な呼び出しを実現する。
文字列
Java呼び出しよりもclojure.stringの関数
文字列処理は、Java呼び出しや独自実装よりも、clojure.stringの関数を使うほうが好ましい。
;; 良い
(clojure.string/upper-case "bruce")
;; 悪い
(.toUpperCase "bruce")
|
Note
|
Clojure 1.8でclojure.stringにいくつかの新しい関数(index-of, last-index-of, starts-with?, ends-with?, includes?)が追加されました。古いバージョンのClojureへの対応が必要な場合は、これらの使用を避けるべきです。
|
例外
既存の例外型の再利用
既存の例外型を再利用する。慣用的なClojureのコードでは、例外を投げる際は標準的な型の例外を使用する。(例:java.lang.IllegalArgumentException, java.lang.UnsupportedOperationException, java.lang.IllegalStateException, java.io.IOException)
;; 良い - データを運ぶ例外(clojure.lang.ExceptionInfo)の生成には ex-info を使う
(throw (ex-info "Invalid input" {:value x :reason :negative}))
;; 良い - Javaの標準例外型を使う
(throw (IllegalArgumentException. "x must be positive"))
;; 悪い - さしたる理由もなく独自の例外型を定義している
(deftype MyCustomException [msg]
...)
finallyよりもwith-openが好ましい
finallyよりもwith-openのほうが好ましい。with-openは例外が発生した場合でもリソースが確実にクローズされることを保証する。また、定型的なコードを減らせる上にクリーンアップ処理を忘れるリスクもない。
;; 良い
(with-open [rdr (clojure.java.io/reader "file.txt")]
(slurp rdr))
;; 悪い - 冗長で、間違えやすい
(let [rdr (clojure.java.io/reader "file.txt")]
(try
(slurp rdr)
(finally
(.close rdr))))
Throwable のキャッチ
Throwableクラスをキャッチしてはいけない。これはJavaにおけるすべてのエラーおよび例外のスーパークラスであり、キャッチするとあらゆるエラーを捕捉して隠蔽してしまうことになる。Errorクラスに関するJavaのドキュメントでは、「ErrorはThrowableのサブクラスで、通常のアプリケーションであればキャッチすべきではない深刻な問題を示しています 」と説明されている。
状況によってはエラーをキャッチする必要があるが、そのような場合はキャッチしたいエラーを指定すること。
;; 良い
(try (foo)
(catch ExceptionInfo ex ...)
(catch AssertionError t ...))
;; 悪い
(try (foo)
(catch Throwable t ...))
マクロ
関数でできるならマクロを書かない
その処理が関数でできるならマクロを書かない。マクロはmap, filter, compといった高階関数に渡すことができないうえに、コードについての考察を難しくする。評価を制御する必要がある場合にのみ、マクロを使用すること。
;; 良い - 単純な関数で問題なく実現できます
(defn square [x]
(* x x))
;; 悪い - 関数で実現できることをマクロで実装
(defmacro square [x]
`(* ~x ~x))
マクロを書く前に使い方を書く
まずマクロの使用例を作成し、その後でマクロを作る。最初に望ましい呼び出し方を書くことはクリーンなAPIを設計するのに役立ち、マクロの過剰な作り込みを回避する。
;; Step 1: マクロをどのように使いたいかを書く
(with-retry {:retries 3 :delay 1000}
(fetch-data url))
;; Step 2: 次に、その使い方を実現するためのマクロを作る
(defmacro with-retry [{:keys [retries delay]} & body]
...)
複雑なマクロの分割
可能な限り、複雑なマクロはより小さな関数に分割する。ヘルパー関数は独立してテスト可能で、他のコードから呼び出せるが、マクロの内部に実装してしまうとそのようなことは出来なくなる。
;; 良い - ヘルパー関数がコード生成の一部を担当する
(defn- emit-constructor [name fields]
`(defn ~(symbol (str "make-" name)) [~@fields]
(new ~name ~@fields)))
(defn- emit-predicate [name]
`(defn ~(symbol (str name "?")) [x#]
(instance? ~name x#)))
(defmacro defentity [name & fields]
`(do
(defrecord ~name [~@fields])
~(emit-constructor name fields)
~(emit-predicate name)))
;; 悪い - すべてを含んだ一つの大きなマクロ
(defmacro defentity [name & fields]
`(do
(defrecord ~name [~@fields])
(defn ~(symbol (str "make-" name)) [~@fields]
(new ~name ~@fields))
(defn ~(symbol (str name "?")) [x#]
(instance? ~name x#))))
構文糖衣としてのマクロ
通常、マクロは単なる構文糖衣として提供するべきで、その中核となる処理は普通の関数として実装すべきだ。そうすることで、合成可能性が向上する。
;; 良い - マクロは関数に対する薄い糖衣になっている
(defn perform-transaction [db-spec func]
(let [conn (get-connection db-spec)]
(try
(.setAutoCommit conn false)
(let [result (func conn)]
(.commit conn)
result)
(catch Exception e
(.rollback conn)
(throw e))
(finally
(.close conn)))))
(defmacro with-transaction [binding & body]
`(perform-transaction ~(second binding)
(fn [~(first binding)] ~@body)))
;; 悪い - すべての処理ロジックがマクロの内部に入っている
(defmacro with-transaction [binding & body]
`(let [conn# (get-connection ~(second binding))]
(try
(.setAutoCommit conn# false)
(let [~(first binding) conn#
result# (do ~@body)]
(.commit conn#)
result#)
(catch Exception e#
(.rollback conn#)
(throw e#))
(finally
(.close conn#)))))
構文クォート
自分でリストを組み立てるよりも、構文クォートを使用するほうが好ましい。構文クォート(バックティック/バッククォート)の使用は、シンボルを自動的に修飾し、意図しない変数のキャプチャを防ぎ、ネストしたlistやconsの呼び出しよりもはるかに読みやすい。
;; 良い - 構文クォートにより生成される構造が明瞭である
(defmacro when-not [test & body]
`(when (not ~test)
~@body))
;; 悪い - 手作業によるリスト構築は結果を理解するのが難しい
(defmacro when-not [test & body]
(list 'when (list 'not test)
(cons 'do body)))
一般的なメタデータ
このセクションでは、Clojureの開発ツールが活用可能な名前空間やvarに対する一般的なメタデータを取り上げる。
:added
公開APIがライブラリに追加されたことを示す最も一般的な方法は、:addedメタデータを使うことだ。
(def ^{:added "0.5"} foo
42)
(ns foo.bar
"A very useful ns."
{:added "0.8"})
(defn ^{:added "0.5"} foo
(bar))
|
Tip
|
セマンティックバージョニングを使っているなら、パッチバージョンを省略するのは良い考えだ。つまり、0.5.0ではなく0.5を使うべきだ。これは、バージョンに関連するすべてのメタデータに当てはまる。
|
:changed
ライブラリの公開APIがいつ変更されたかを記録する最も一般的な方法は、:changedメタデータを使うことだ。このメタデータは公開されているvarに対してのみ意味をなすものである。また、公開APIの挙動を変更することは一般的に好ましくないので使用は控えめにする。
それでもなお変更する場合は、APIの利用者にその旨を明確にしておくのが最善だ。
(def ^{:added "0.5"
:changed "0.6"} foo
43)
:deprecated
公開APIが非推奨となったことを示す最も一般的な方法は、:deprecatedメタデータを使うことだ。通常、値には非推奨となったバージョン(ライブラリなど、バージョン管理されているソフトウェアの場合)を指定するか、単にtrue(Webアプリケーションなど、バージョン管理されていない場合)を指定する。
;;; 良い
;;
;; バージョンがある場合
(def ^{:deprecated "0.5"} foo
"Use `bar` instead."
42)
(ns foo.bar
"A deprecated ns."
{:deprecated "0.8"})
(defn ^{:deprecated "0.5"} foo
(bar))
;; バージョンがない場合
(defn ^:deprecated foo
(bar))
;;; 悪い
;;
;; 非推奨を示すためにドキュメント文字列を使っている
(def foo
"DEPRECATED: Use `bar` instead."
42)
(ns foo.bar
"DEPRECATED: A deprecated ns.")
:superseded-by
多くの場合:superseded-byは:deprecatedと組み合わせて使用する。これは、非推奨となったAPIに代わる新しいAPIが存在するからだ。
通常、指定するvarについては新しい名前が同じ名前空間に存在する場合は修飾していない名前を、そうでない場合は完全修飾した名前を使う。
;; バージョンがある場合
(def ^{:deprecated "0.5"
:superseded-by "bar"} foo
"Use `bar` instead."
42)
(ns foo.bar
"A deprecated ns."
{:deprecated "0.8"
:superseded-by "foo.baz"})
(defn ^{:deprecated "0.5"
:superseded-by "bar"} foo
(bar))
;; バージョンがない場合
(defn ^{:deprecated true
:superseded-by "bar"} foo
(bar))
|
Tip
|
新しいAPIには:supersedesメタデータを付加することも検討すべきだ。これは基本的に:superseded-byとは逆の意味を持っている。
|
:see-also
ときには、ライブラリの利用者が興味を持ちそうな関連するvarや名前空間を提示したいかもしれない。そのような場合、:see-alsoメタデータに関連するアイテムをベクターにした値を指定するのが最も一般的な方法だ。varについていえば、同じ名前空間にある項目ならば完全修飾で指定する必要はない。
;; 同じ名前空間内のvarへの参照
(def ^{:see-also ["bar" "baz"]} foo
"A very useful var."
42)
;; 他の名前空間に存在するvarへの参照
(defn ^{:see-also ["top.bar" "top.baz"]} foo
(bar))
|
Note
|
多くのClojure用プログラミングツールはドキュメント文字列から他のvarへの参照を抽出しようとするが、:see-alsoメタデータを使うほうがよりシンプルかつ明示的である。
|
:no-doc
Codoxやcljdocなどのドキュメンテーションツールは:no-docメタデータを認識する。varや名前空間が:no-docメタデータを持つ場合、ツールに対して生成されるAPIドキュメントからは当該要素を除外すべきであることを示している。
APIドキュメントから名前空間全体を除外する:
(ns ^:no-doc my-library.impl
"Internal implementation details")
...
ドキュメント化対象の名前空間の中からvarを除外する:
(ns my-library.api)
;; プライベート関数につきドキュメント化対象から除外
(defn- clearly-private []
...)
;; パブリック関数でも :no-doc メタデータを持っているなら同様に除外
(defn ^:no-doc shared-helper []
...)
;; このパブリック関数はドキュメント化される
(defn api-fn1
"I am useful to the public"
[]
...)
インデント用メタデータ
他のLisp方言とは異なり、Clojureはマクロのインデントを指定する標準的なメタデータは存在しない。2015年、CIDERはメタデータに基づいたツールに依存しないインデントの指定方法を提案した[9]。簡単な例をいくつか示す:
;; 同じ名前空間内のvarへの参照
(defmacro with-in-str
"[DOCSTRING]"
{:style/indent 1}
[s & body]
...cut for brevity...)
これはインデント処理エンジンに対して、「1つの通常のパラメーターと、その後に本体が続くマクロ」であることを指示している。
;; メタデータ無し(通常の関数として字下げ処理)
(dop-iin-str some-string
foo
bar
baz)
;; メタデータ有り(1つの通常のパラメーターと、その後に本体が続くマクロとして字下げ処理)
(with-in-str some-string
foo
bar
baz)
残念ながら、2020年時点で:style/indentメタデータは広く受け入れられておらず、多くのエディターやIDEは一般的なマクロのインデント規則を単にハードコーディングしている。
|
Note
|
このインデントの考え方(セマンティック・インデント)は、メタデータの追加とツールのサポートが必要となるためClojureコミュニティで議論の的になっています。この考え方はLispコミュニティで長い伝統があるにもかかわらず、一部の人たちは関数とマクロの区別をやめて、すべてを統一した固定インデントで字下げをすべきだと主張しています。この記事はそのような主張に関する代表的な資料の一つです。 |
コメント
良いコードとは、それ自体が最良のドキュメントになっているものだ。コメントを付けようとしたとき、自分の胸に聞いてみるといい、「どうやってコードを改良して、このコメントを不要にできるだろうか?」ってね。より美しくするために、コードを改良してからドキュメント化するんだ。
自己説明的なコード
コードを、可能な限りそれ自体で意味が伝わるように努める。これができない場合は、このセクションの残りのガイドラインに従うこと。
見出しコメント
見出しコメントには最低4つのセミコロンを用いる。これらは通常、コードの主要なセクションについて概要を示したり区切ったりするほか、重要な考えを説明するのに使われる。セクションコメントの後には、後述するトップレベルのコメントが続くことがよくある。
;;;; セクションコメント/見出し
;;; Foo...
;;; Bar...
;;; Baz...
トップレベルのコメント
トップレベルのコメントには3つのセミコロンを用いる。
;;; ここはトップレベルのコメント。
;;; どんな定義にも含まれない場所に書かれる。
(defn foo [])
|
Note
|
古典的なLispの慣習ではトップレベルのコメントに;;;を使いますが、実際のClojureのコードでは;;や;を使っているケースが散見されます。
|
コード部分(行)コメント
特定のコード部分にコメントを書くときは、当該コード部分の直前にコードとインデントを揃えて、2つのセミコロンを用いる。
(defn foo [x]
;; 行コメント/コード部分コメント
x)
|
Note
|
古典的なLispの慣習では行コメントに;;を使いますが、実際のClojureのコードでは;だけを使っているケースが散見されます。
|
行末(インライン)コメント
行末コメントには1つのセミコロンを用いる。
(defn foo [x]
x ; 行コメント/コード部分コメント
)
複数の閉じ括弧が独立した行にぶら下がってしまう場合は、使用を避けること。
セミコロンのスペース
セミコロンとそれに続くテキストの間には、常に少なくとも1つのスペースを入れる。
;;;; 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))
英語の文法
2単語以上のコメントは大文字で始め、句読点を用いる。各文は1つのスペースで分ける。
;; This is a good comment.
;; this is a bad comment
もちろんコメントにおいて句読点は最も重要な要素ではないが、コメントを読む人にとって少しだけより良い体験(読みやすさ)を提供することができる。
コメントの更新
コメントは常に最新の状態を保たなければならない。いまの状況に合っていない古いコメントは、コメントがないことよりも害悪だ。
#_リーダーマクロ
特定のフォームをコメントアウトする必要があるときは、通常のコメントではなく#_リーダーマクロを用いるほうが好ましい。
;; 良い
(+ foo #_(bar x) delta)
;; 悪い
(+ foo
;; (bar x)
delta)
コメントよりリファクタリング
良いコードというのは面白いジョークのようなものだ。説明する必要がない。
悪いコードを説明するためにコメントを書くことを避ける。コードをリファクタリングし、それ自体で意味が伝わるようにする。(「やるか、やらぬかだ。やってみるなどない」 — ヨーダ)
コメントアノテーション
アノテーションは直前に
アノテーションは通常、当該コードの直前に書かれるべきだ。
;; 良い
(defn some-fun
[]
;; FIXME: Replace baz with the newer bar.
(baz))
;; 悪い
;; FIXME: Replace baz with the newer bar.
(defn some-fun
[]
(baz))
アノテーションキーワード
アノテーションキーワードの後にはコロンとスペースを入れ、その後に問題の内容を説明する記述を続ける。
;; 良い
(defn some-fun
[]
;; FIXME: Replace baz with the newer bar.
(baz))
;; 悪い - アノテーションの後にコロンがない
(defn some-fun
[]
;; FIXME Replace baz with the newer bar.
(baz))
;; 悪い - コロンの後にスペースがない
(defn some-fun
[]
;; FIXME:Replace baz with the newer bar.
(baz))
アノテーションのインデント
問題の内容説明が複数行にわたる場合、2行目以降は1行目と同じだけインデントするべきだ。
;; 良い
(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 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 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
後日追加すべき要素や機能が欠けている場合はTODOを使う。
(defn process-order [order]
;; TODO: Add support for discount codes.
(calculate-total order))
FIXME
コードが壊れていて、修正の必要がある箇所にはFIXMEを使う。
(defn calculate-total [order]
;; FIXME: This doesn't account for tax in all regions.
(reduce + (map :price (:items order))))
OPTIMIZE
パフォーマンスの問題になりうる、遅かったり非効率なコードにはOPTIMIZEを使う。
(defn find-duplicates [coll]
;; OPTIMIZE: O(n^2) — consider using a set for large collections.
(for [x coll y coll :when (and (not (identical? x y)) (= x y))]
x))
HACK
疑問の余地があるコーディングがされており、リファクタリングで解消すべき箇所(いわゆる「コード・スメル」)の指摘にはHACKを使う。
(defn user-name [request]
;; HACK: Parsing the token manually until we add proper auth middleware.
(-> request :headers (get "authorization") (subs 7) decode-token :name))
REVIEW
意図した通りに機能しているかどうかを確認すべき箇所にはREVIEWを使う。例:REVIEW: Are we sure this is how the client does X currently?
(defn retry [f n]
;; REVIEW: Should we add exponential backoff here?
(loop [i n]
(or (try (f) (catch Exception _ nil))
(when (pos? i) (recur (dec i))))))
カスタムアノテーション
必要に応じて、その他のアノテーションキーワードを用いる。ただし、プロジェクトのREADMEなどに必ず記載する。
ドキュメント
Clojureコードをドキュメント化するための主な手段は、ドキュメント文字列(docstring)だ。多くの定義フォーム(例:def, defn, defmacro, ns)はドキュメント文字列をサポートしており、そのvarがパブリックであるかプライベートであるかに関わらず、これを活用するのが一般的に良い考えである。
もし定義フォームがドキュメント文字列を直接的にサポートしていなくても、メタデータの:doc属性にドキュメントを記述することができる。
このセクションでは、Clojureコードをドキュメント化する際の一般的な慣習とベストプラクティスを紹介する。
ドキュメント文字列が好ましい
フォームがドキュメント文字列を直接的にサポートしている場合、:docメタデータよりもそれを用いるほうが好ましい。
;; 良い
(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)
ドキュメント文字列の要約
ドキュメント文字列の最初の行は、大文字で始まる完結した文で、そのvarを簡潔に説明するものにする。これによって、ツール(Clojure用エディターやIDE)が様々な場面でドキュメント文字列の要約を容易に表示できるようになる。
;; 良い
(defn frobnitz
"This function does a frobnitz.
It will do gnorwatz to achieve this, but only under certain
circumstances."
[]
...)
;; 悪い
(defn frobnitz
"This function does a frobnitz. It will do gnorwatz to
achieve this, but only under certain circumstances."
[]
...)
ドキュメント文字列でのMarkdownの利用
cljdocなどの有用なツールはドキュメント文字列内でMarkdown記法をサポートしているので、フォーマットをきれいに整えるのに利用すると良い。
;; 良い
(defn qzuf-number
"Computes the [Qzuf number](https://wikipedia.org/qzuf) of the `coll`.
Supported options in `opts`:
| key | description |
| --------------|-------------|
| `:finite-uni?`| Assume finite universe; default: `false`
| `:complex?` | If OK to return a [complex number](https://en.wikipedia.org/wiki/Complex_number); default: `false`
| `:timeout` | Throw an exception if the computation doesn't finish within `:timeout` milliseconds; default: `nil`
Example:
```clojure
(when (neg? (qzuf-number [1 2 3] {:finite-uni? true}))
(throw (RuntimeException. \"Error in the Universe!\")))
```"
[coll opts]
...)
位置パラメーターのドキュメント化
すべての位置パラメーターをドキュメント化し、それらをバッククォート`で囲む。そうすることで、エディターやIDEがパラメーターを識別できるようになり、より高度な機能を提供できる可能性がある。
;; 良い
(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]
...)
ドキュメントの参照
ドキュメント文字列内でのvarへの参照は、ツールが識別できるように`で囲む。リンクを張りたい場合は[[..]]で囲む。
;; 良い
(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.
See also [[kangaroo]]."
[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.
See also kangaroo."
[x]
...)
ドキュメント文字列の文法
ドキュメント文字列は正しい英語の文で構成されるべきだ。すべての文は大文字で始まり、文法的に一貫していて、適切な句読点で終わる。また、各々の文の間には1つのスペースをはさむ。
;; 良い
(def foo
"All sentences should end with a period (or maybe an exclamation mark).
The sentence should be followed by a space, unless it concludes the docstring.")
;; 悪い
(def foo
"all sentences should end with a period (or maybe an exclamation mark).
The sentence should be followed by a space, unless it concludes the docstring.")
ドキュメント文字列のインデント
複数行にわたるドキュメント文字列は、2つのスペースでインデントする。
;; 良い
(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 docstring 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 docstring makes
it easier for tooling to display it correctly.")
ドキュメント文字列の先頭・末尾の空白
ドキュメント文字列の最初と最後には余計な空白を入れない。
;; 良い
(def foo
"I'm so awesome."
42)
;; 悪い
(def silly
" It's just silly to start a docstring with spaces.
Just as silly as it is to end it with a bunch of them. "
42)
ドキュメント文字列は関数名の後ろ
ドキュメント文字列を付加する際、とりわけ前述したフォームを用いる関数に対しては、ドキュメント文字列を引数ベクターの後ろではなく関数名の後ろに置くことに注意する。前者は文法的には間違っておらずエラーにもならないが、そのvarにドキュメントは付加されず、関数本体に1つのフォームとしてその文字列が含まれることになる。
;; 良い
(defn foo
"docstring"
[x]
(bar x))
;; 悪い
(defn foo [x]
"docstring"
(bar x))
|
Note
|
|
テスト
テストディレクトリの構造
テストコードはソースコードとは別のディレクトリ(通常はsrc/yourproject/ではなくtest/yourproject/)に配置する。ビルドツールは必要に応じてこれらのディレクトリを用意してくれる。ほとんどのテンプレートは自動的にこの処理をおこなう。
テストの名前空間
名前空間はyourproject.something-testのように命名する。通常、ファイルはtest/yourproject/something_test.clj(あるいは.cljc, cljs)として作成する。
テストの命名規則
clojure.testを用いるときは、deftestでテストを定義し、something-testと名付ける。
;; 良い
(deftest something-test ...)
;; 悪い
(deftest something-tests ...)
(deftest test-something ...)
(deftest something ...)
コンテキストのためにtestingブロック
テスト失敗時の出力にコンテキスト(状況)を含めるため、関連するアサーションをtestingの下にまとめる。これによりアサーションそのものを確認しなくても、どのテストシナリオで失敗したのかが明確になる。
;; 良い
(deftest user-validation-test
(testing "rejects blank names"
(is (not (valid? {:name ""}))))
(testing "accepts valid names"
(is (valid? {:name "Bruce"}))))
;; 悪い - テスト失敗時にコンテキストが出力されない
(deftest user-validation-test
(is (not (valid? {:name ""})))
(is (valid? {:name "Bruce"})))
情報提供型のアサーションメッセージが好ましい
アサーションだけで失敗の内容を説明できない場合は、isにメッセージを追加する。
;; 良い - 何が悪かったかを示すエラーメッセージ
(is (= 200 (:status response)) "Expected HTTP 200 for valid input")
;; 大丈夫 - 明確なアサーションにメッセージは不要
(is (true? (even? 4)))
テーブル駆動テストにはare
一つの機能に対して多くの入力でテストする場合、isで繰り返すよりareを使うほうが好ましい。
;; 良い
(deftest palindrome?-test
(are [s expected] (= expected (palindrome? s))
"racecar" true
"hello" false
"madam" true
"" true))
;; 悪い - 反復的だ
(deftest palindrome?-test
(is (true? (palindrome? "racecar")))
(is (false? (palindrome? "hello")))
(is (true? (palindrome? "madam")))
(is (true? (palindrome? ""))))
deftestごとに1つのコンセプトでテスト
それぞれのdeftestは1つの論理的な挙動をカバーするべきだ。各エッジケースについては、ケース毎にdeftestを分けるのではなくtestingを使ってサブケースとしてまとめる。
with-redefsの使用は控えめに
with-redefsは外部との境界(HTTP、データベース、時計など)に限って使用する。それよりも、依存対象を引数で受け取るよう関数を設計するほうが好ましい。
;; 良い - 依存対象を引数で渡している
(defn fetch-users [http-get]
(http-get "/api/users"))
(deftest fetch-users-test
(is (= [{:name "Bruce"}]
(fetch-users (constantly [{:name "Bruce"}])))))
;; 統合テスト用なら大丈夫
(defn fetch-users []
(http/get "/api/users"))
(deftest fetch-users-integration-test
(with-redefs [http/get (constantly {:body [{:name "Bruce"}]})]
(is (= [{:name "Bruce"}] (fetch-users)))))
例外発生のテスト
コードが期待した例外を投げることを確認するには、thrown?やthrown-with-msg?を使う。
;; 良い
(deftest division-test
(is (thrown? ArithmeticException (/ 1 0)))
(is (thrown-with-msg? ExceptionInfo #"Invalid" (validate! nil))))
構造的なデータに対するアサーションにはmatch?
生成されたフィールドや動的に値が変化するフィールド(IDやタイムスタンプ)を含むマップを返す関数をテストする場合は、matcher-combinatorsが提供するmatch?の使用を検討する。これにより、過度に詳細を指定することなく構造や型に対してアサーションが可能になり、失敗時には明確な差分が提供される。
(require '[matcher-combinators.test])
(require '[matcher-combinators.matchers :as m])
;; 良い - 構造と型をアサーションの対象とし、新しいフィールドが追加されても大丈夫
(is (match? {:id uuid?
:name "Alice"
:status :active
:created-at inst?}
(create-user! {:name "Alice"})))
;; 悪い - 新しいフィールドが追加されると動作せず、脆い
(is (= {:id "abc-123"
:name "Alice"
:status :active
:created-at #inst "2024-01-01"}
(create-user! {:name "Alice"})))
;; これも悪い - 構造に関するコンテキストが失われている
(let [result (create-user! {:name "Alice"})]
(is (= "Alice" (:name result)))
(is (= :active (:status result)))
(is (uuid? (:id result))))
例外データに対してはthrown-match?
発生した例外のデータをマップとして持っているex-info例外のテストには、try/cathchの定型コードよりも matcher-combinators のthrown-match?の使用が好ましい。これはmatch?と同様に、例外の構造的なデータに対してアサーションを行う。
;; 良い
(is (thrown-match? ExceptionInfo
{:type :validation-error :field :email}
(validate! {:email "bad"})))
;; 悪い - 冗長で間違いやすい
(is (thrown? ExceptionInfo
(try (validate! {:email "bad"})
(catch ExceptionInfo e
(is (= :validation-error (:type (ex-data e))))
(throw e)))))
ライブラリの構成
ライブラリの識別子
他の人が使えるようにライブラリを公開する場合、Central RepositoryのガイドラインにしたがってgroupIdとartifactIdを選ぶ。これにより名前の衝突が避けられ、幅広い利用が促進される。Componentが良い例で、識別子はcom.stuartsierra/componentだ。
異なるアプローチとして、groupIdにドメインではなくプロジェクト名(あるいは組織名)がよく用いられる。以下に例を示す。
-
cider/cider-nrepl -
nrepl/nrepl -
nrepl/drawbridge -
clj-commons/fs
依存の最小化
不必要な依存を避ける。たとえば、何百もの使う予定のないvarを含んだライブラリに依存するよりも、3行のユーティリティ関数をプロジェクトにコピーしてしまうほうが良い。
ツール
スタイルガイドにまつわる課題の一つは、すべてのガイドラインを覚えて一貫して適用するのが難しいことだ。何しろ、私たちは人間だから。幸いなことに、作業のほとんどを実行してくれるツールが数多く存在する。
|
Tip
|
こうしたツールを継続的インテグレーション(CI)に組み込むのは素晴らしいアイデアだ。これによりプロジェクト内のすべてのコードが、目標とするスタイルに統一されていることが保証される。 |
Lintツール
慣用的なClojureコードを書くのを助けてくれるLintツールが、Clojureコミュニティによって作られている:
-
kibitはClojure向けの静的コード解析ツールだ。core.logicを利用して、より慣用的な関数やマクロに置き換えられる可能性のあるコードパターンを検索する。
-
clj-kondoは、このスタイルガイドに基づいて多くの非推奨パターンを発見し、改善案を提案してくれるLintツールだ。
コードフォーマッター
ほとんどのClojure用エディターやIDEはここで示されているレイアウトのガイドラインに従ってコードを整形できるが、コマンドラインで動作するフォーマッターを用意しておくと便利である。このスタイルガイドで推奨されている形式に沿ってClojureのコードを整形するにあたり、優れた働きをする選択肢がいくつか存在する:
|
Note
|
エディターに関していえば、Emacsのclojure-modeはスタイルガイドに記載されているとおりに自動的にコードを整形します。他のエディターでは、同じ結果を得るために設定の調整が必要となる場合があります。
|
歴史
このスタイルガイドはBozhidar Batsovにより、彼がRubyコミュニティで立ち上げた類似プロジェクトの成功を受けて2013年に開始されました。
BozhidarはClojureと優れたプログラミングスタイルに強い情熱を持っていて、Clojure library coding guidelinesの内容と、Java、Python、Rubyなどのプログラミング言語のスタイルガイドで通常扱われている内容とのギャップを埋めることを目指していました。現在もBozhidarはこのガイドの主任編集者ですが、プロジェクトを支える編集チームが存在しています。
ガイドを公開してから、世界中の優れたClojureコミュニティのメンバーから多くのフィードバックをいただいています。ご提案やご支援に感謝申し上げます! みんなの力をあわせれば、世界中のすべてのClojure開発者にとって有益なリソースを作り上げることができるでしょう。
着想の源
多くの人々、書籍、プレゼンテーション、記事、他のスタイルガイドが、このスタイルガイドに影響を与えています。そのうちのいくつかを紹介します:
編集チーム
経験豊富なClojure開発者からなる編集チームによってClojureスタイルガイドは管理されており、寄せられるあらゆる意見(フィードバックや提案など)を集約し、誰もが参考にできるより良いリファレンスとなることを目指しています。
貢献
このスタイルガイドはまだ作成途中です。ガイドラインの中には例が欠如していたり、例が十分にわかりやすくない物があります。こうしたガイドラインを改善することは、Clojureコミュニティに対する素晴らしい(そして簡単な)貢献です!
いずれ(うまくいけば)これらの問題は改善されるでしょう。いまのところは、そういった不具合があることを念頭に置いてください。
このスタイルガイドはまだまだ書き換えることができます。Clojureのコーディングスタイルに関心のある皆さんと一緒に取り組み、最終的にはClojureコミュニティ全体にとって有益な情報源を作り上げたいと思っています。
遠慮なくチケットをオープンしたり改良案のプルリクエストを送ってください。どうかよろしくお願いします。
下記のプラットフォームを通して、金銭的にこのスタイルガイド(およびCIDER、nREPL、orchardといった私のClojureプロジェクト)を支援することもできます:
どのように貢献すればよいですか?
簡単です、以下の貢献のためのガイドラインに従ってください:
-
リポジトリbbatsov/clojure-style-guideをGitHub上でフォークします。
-
内容の追加や誤りの修正は新しいブランチを切っておこなってください。
-
変更内容について詳しい説明をつけてコミットします。
-
変更したブランチをGitHubにプッシュします。
-
Pull Requestを送ってください。
奥付
このガイドはAsciiDocで記述されており、AsciiDoctorでHTML版に変換して公開されています。HTML版はGitHub Pagesで提供されています。
当初、このガイドはMarkdownで記述されていましたが、2019年にAsciiDocに変換されました。
ライセンス
この作品は、Creative Commons Attribution 3.0 Unported Licenseの下でライセンスされています。
広めてください
コミュニティドリブンのスタイルガイドは、その存在を知らないコミュニティではあまり役に立ちません。どうか、このガイドについてツイートをして、あなたの友達や同僚と共有してください。皆様から寄せられるコメントや提案、意見の一つひとつが、このガイドを少しずつより良いものにしてくれます。私たちは最高のスタイルガイドを作りあげたい — そう思いませんか?