clojureのコーディングスタイル
vnctst-audio3の開発ドキュメント関連の為に、自分のコーディングスタイルをどこかに明記する必要が出たので、ここに記す。
筆者のclojureのコーディングスタイル
筆者は clojure-style-guide に大部分は従うが、一部従わない、独自のコーディングスタイルで記述しています。
もし筆者のclojure製プロジェクトのリポジトリに対してpull-req等してもらう際には、これに従ってもらいます。
(なお、元々が筆者のものではないソースに関しては、基本的にはこのコーディングスタイルを適用していません。最も重要なのは「ソース内でコーディングスタイルが統一されている」事であるので、元々のスタイルに合わせている筈です。うっかりしている時もありますが…)
このコーディングスタイルは、たまにルールを更新する場合があります。
以下に、clojure-style-guide
と合致しない部分のコーディングスタイルを列挙します(つまり、以下で明記していない部分についてはclojure-style-guide
に従います)。
and
or
->
等のマクロも、do
等と同じインデントルールとする- 何故ならこれらは、引数の評価順が通常の関数とは異なる為(引数が状況によって評価されないケース等がある)。そのようなものを関数と同じインデントルールにするのは一貫性がないと考える。
- 関数と同様のインデントルールとしてもよいマクロは、基本的には、単に関数をインライン展開するような目的で作られたマクロ(つまり、引数の評価順が通常の関数と同じになるように展開されるマクロ)ぐらいだが、これは本質的にはマクロである必要性はなく、このようなマクロの使い方はなるべく避ける。
;; Good (and foo bar baz) (or very-long-variable-a very-long-variable-b very-long-variable-c) (defmacro my-let [bindings & bodies] ...) (my-let [v 1] (inc v)) ;; Bad (関数でないにも関わらず、関数のインデントルールと同じ) (and foo (do (prn :debug) bar) baz)
- 関数の第一引数を次の行に書くのをなるべく避ける。またその際のインデント量は1ではなく2とする
- vectorの場合は、最初の要素が特別扱いされる要素は別にないので1インデントの方が「どの要素も平等」という事が示されていて良いと考える。が、いわゆるLispのリストを式として評価する場合、リストの最初の要素が特別(要は暗黙の
funcall
がある)なので、「最初の要素は特別である」事を明示する意味でも2インデントが好ましいと考える。そしてこれは前述のスペシャルフォーム/マクロのインデント量とも一致する。 - 「seqはcons cellとは違うのだから、readした段階ではvectorもseqも『どの要素も平等』なのでは?」という反論もあるだろうが、それを考える場合、
do
やlet
等のマクロやスペシャルフォームも同じインデントルールにすべきように思える(readした段階ではこれらもマクロ展開等は行われない為)。もしここで更に「do
やlet
は構文だから」という反論をするのはLispの拡張性を分かっていない。Lispではマクロで自由に構文を増やせるのだから、「このマクロは構文的な扱いで、あのマクロは構文的な扱いでない」というのをいちいち設定するのはルールの爆発的増加を起こす。ルールはシンプルな方が良い。だから「マクロのインデントルールは一律でこう」「関数のインデントルールは一律でこう」という二つだけに絞りたい。 - あと、単に1インデントだと分かりづらい為。
- これについては、本当はhiccup類の
[:h1 ...]
とかでも1インデントだと分かりづらいと思っているのだが、こっちは前述の「どの要素も平等」という事を示しつつ2インデントにするベストな方法がないので諦めている。また本質的にはhiccup構文側の仕様のミスだとも思っている(本当は「どの要素も平等」ではない。最初の要素はほとんどのケースでタグ名となり必要不可欠(関数と大体同様の立ち位置))
- これについては、本当はhiccup類の
- このルールは第一引数の場合にのみ適用する。第二引数以降を次の行に書く時のインデントは通常通り、その直前の引数の位置と揃える
- ただし、このルールは既存のLisp系でも結構異端で、エディタ設定が面倒なケースが多いようだ。なので、そもそもの元凶である「関数の第一引数を次の行に書く」という状況自体をなるべく避けるようにするのがベスト。
- vectorの場合は、最初の要素が特別扱いされる要素は別にないので1インデントの方が「どの要素も平等」という事が示されていて良いと考える。が、いわゆるLispのリストを式として評価する場合、リストの最初の要素が特別(要は暗黙の
;; Good (very-long-function-a foo bar baz) (very-long-function-a foo bar baz) ;; Ok (ただしなるべく避ける) (very-long-function-a foo bar baz) (very-long-function-a foo bar baz) ;; Bad (very-long-function-a foo bar baz) (very-long-function-a foo bar baz) ;; 参考 (関数以外での類似インデント) [:foo :bar :baz] {:foo 1 :bar 2 :baz 3} '(aaa bbb ccc) (do (foo) (bar) (baz))
- 連続する閉じ括弧を一つの行にまとめるかどうかは、状況に応じて判断する
;; Good (今後に末尾に追加していく可能性が高い場合) (ns foo.bar (:require [clojure.string :as string] [foo.bar.baz :as baz] )) (def modes #{:normal :super :safe :danger :fatal }) ;; Ok (bazの後に追加するとdiffが二行になる。しかし許容はできる) (ns foo.bar (:require [clojure.string :as string] [foo.bar.baz :as baz])) ;; Good (末尾の値自体は今後変更しない可能性が高い場合) (defn foo! [x] (let [y ... result ...] ... ;; 今後に副作用処理をここに追加する想定 result)) ; しかし追加しても最後に返す変数は変化しない筈 (def trump-marks #{:spade :heart :diamond :club :joker}) ; トランプの種類は増えない ;; Bad (無駄な改行) (defn foo! [x] (let [y ... result ...] ... ;; 今後に副作用処理をここに追加する想定 result ;; 後からここに式などを追加するような事は想定しづらい ))
(when-not (empty? v) ...)
を許容するclojure-style-guide
にはnil punning
を使うように書かれており、一時はこれに従っていたものの、「逆に分かりづらい」という結論になった
;; Good (長いが英文として読める) (when-not (empty? v) ...) ;; Not Good (短いが英文として読めず、逆に分かりづらい) (when (seq v) ...)
- 基本的にはスレッディングマクロは使わない
;; Bad (コンパイルエラー) (-> 1 #(- 5 %) inc #(/ % 3) str) ;; Very Bad (コンパイルは通るが意図した動作をしない) (->> 1 #(- 5 %) inc #(/ % 3) str) ;; True, but confused (正しくはこう書かないといけない) (let [div-3 #(/ % 3)] (->> 1 (- 5) inc div-3 str)) ;; Ok (回りくどいが間違いは少ない) (let [r 1 r (- 5 r) r (inc r) r (/ r 3) r (str r)] r)
- ある特定の行のコードを一時的にコメントアウトする時は、「セミコロン二つ+空白」ではなく「セミコロン一つ」とする
- 簡単に追加/解除できる方を優先する
- あくまで「一時的に」なので、そのようなコードがずっと残るような場合は、別途
(comment ...)
等にして外に出したり、そのコメント自体を消したりするように変更する事(よく忘れて放置しているが…)
;; Ok (let [foo 123] ;(prn :debug foo) ... ;; 式の末尾等、セミコロンでのコメントアウトを行うと式の ;; 構造が壊れるような場合は、一要素コメントアウト書式を使う #_(prn :debug foo))
- トップレベルの行間は感覚的に(大雑把に)空ける
- S式リーダーは行間は気にしないし、編集中に特定行へ移動する際にはエディタのサポート(関数名で検索移動など)を使わない訳がないので、ここは「人間がパッと見た時に分かりやすい」事だけを優先してよい
- 人間はコードブロックを視覚的(二次元的)に捉えるので、「あるコードブロックが別のコードブロックと同じか違うか」を判定できればよい
- どれぐらいの量の改行にするかは「コードブロックの(視覚的な)横幅と縦幅」「隣接する二つのコードブロックの関連度」等によって大雑把に決定する。具体的には以下のような感じ
- コードブロックが視覚的に大きい場合、間の空行もそれなりに多くないと人間は認識しにくい。逆に小さい場合は間の空行は少なくてよい
- 隣接する二つのコードブロックに強い関連があるなら、間の空行は一行もしくはなしにする。そうでなければコードブロックの視覚的な大きさに応じて、二行以上あける
- なお、
clojure-style-guide
には「ある関数の中で空行を入れるスタイル」があるが(cond等)、これは上記のルールを適用していると、うっかり「二つのコードブロックなのか?」と勘違いしてしまうケースがあるので、なるべく「関数の中には空行は入れない」ルールとする。- よって、condの条件と実行式を二行に分けるルールは基本的には使わない
;; Bad (中に空行を入れるようなcondの書き方は避ける) (cond ;; test case 1 (test1 foo bar baz) (long-function-name-which-requires-a-new-line ...) ;; test case 2 (test2 foo bar baz) (another-very-long-function-name ...) :else (the-fall-through-default-case ...))
- core.asyncのchanを返す関数名は末尾に
$
をつける(仮)- core.asyncのchanを返す関数は「それ自体は副作用を持たないけれど、(ほとんどのケースで)副作用処理を内包している」という、末尾に
!
を付けるのも付けないのも迷う存在である為。 - よって、それを示す為に、末尾に
!
の代わりに$
を付けるルールとした - ただし、「末尾に
$
を付ける」ルールはcljs環境にてjsの標準キーワード回避のルールと衝突するケースがあるっぽい(cljs.core/munge
の変換ルールを確認した)。ので今後は他の記号に変更するかも
- core.asyncのchanを返す関数は「それ自体は副作用を持たないけれど、(ほとんどのケースで)副作用処理を内包している」という、末尾に
(defn heavy-rand$ [] (go (<! (async/timeout 1000)) (rand))) (go (println "run heavy rand") (println (<! (heavy-rand$))) (println "done"))