読者です 読者をやめる 読者になる 読者になる

VNCTST games 開発日誌

ゲーム開発日誌

clojureのコーディングスタイル

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も『どの要素も平等』なのでは?」という反論もあるだろうが、それを考える場合、dolet等のマクロやスペシャルフォームも同じインデントルールにすべきように思える(readした段階ではこれらもマクロ展開等は行われない為)。もしここで更に「doletは構文だから」という反論をするのはLispの拡張性を分かっていない。Lispではマクロで自由に構文を増やせるのだから、「このマクロは構文的な扱いで、あのマクロは構文的な扱いでない」というのをいちいち設定するのはルールの爆発的増加を起こす。ルールはシンプルな方が良い。だから「マクロのインデントルールは一律でこう」「関数のインデントルールは一律でこう」という二つだけに絞りたい。
    • あと、単に1インデントだと分かりづらい為。
      • これについては、本当はhiccup類の[:h1 ...]とかでも1インデントだと分かりづらいと思っているのだが、こっちは前述の「どの要素も平等」という事を示しつつ2インデントにするベストな方法がないので諦めている。また本質的にはhiccup構文側の仕様のミスだとも思っている(本当は「どの要素も平等」ではない。最初の要素はほとんどのケースでタグ名となり必要不可欠(関数と大体同様の立ち位置))
    • このルールは第一引数の場合にのみ適用する。第二引数以降を次の行に書く時のインデントは通常通り、その直前の引数の位置と揃える
    • ただし、このルールは既存の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)
  ...)
  • 基本的にはスレッディングマクロは使わない
    • 他言語環境でのmethod chainとは違い、多くのケースで「何番目の引数をスレッディングさせたいか」が関数によってまちまちな為。スレッディングマクロを使う代わりにletで一文字の変数にでも入れて渡した方が使い勝手が良い
    • スレッディングマクロの途中に無名関数を書いて含めてしまうミスが多い
      • 一見書けそうに見えるが、スレッディングマクロ中のseqは「そのsecondもしくはlastに、chain引数が勝手に追加される」仕様である為、無名関数の(fn ...)が勝手に改竄され、->ならコンパイルエラーが出るし、->>なら更に悪い事にコンパイルエラーが出ずに謎の関数オブジェクトが返される
;; 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の変換ルールを確認した)。ので今後は他の記号に変更するかも
(defn heavy-rand$ []
  (go
    (<! (async/timeout 1000))
    (rand)))
(go
  (println "run heavy rand")
  (println (<! (heavy-rand$)))
  (println "done"))