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

(define -ayalog '())

括弧に魅せられて道を外した名前のないプログラマ

Clojure における幾つかの実践的なデバッグ方法

Clojure

まえがき

だいたい 2 週間くらい Clojure を書いているとライブラリのバグを綺麗に踏み抜いたりマクロの沼に引きずりこまれたりと、まぁやたらとデバッグする機会があります。それで先日からライブラリを clone して書きなおして lein install して…などしていたのですが、一緒に Clojure 書いている Clojure チョットデキル人に「そんなことしなくてもいいよ」と色々教えてもらったのでだいたい現時点で僕が知っている幾つかのデバッグ方法を書いておこうと思う。ただし、これは Clojure 一般というよりは Emacs/Cider-mode という環境に依存している部分が多いと思うので他の環境を使っている人はもしかしたらかなり役に立たないかもしれません。アプローチそのものは他の IDE などでも実装されているかもしれませんが僕はよく知らないので悪しからず。

Print デバッグ

原始的な print デバッグですが、 REPL で逐次評価出来るので割と地味に使えたりします。

(defn foo [x]
  (filter #(do
             (println %)
             (even? %))
          (map #(* % %)
               (range x))))
(foo 10)
;0
;1
;4
;9
;16
;25
;36
;49
;64
;81
;=> (0 4 16 36 64)

けどいちいち println とか挟むのも億劫なので spyscope というライブラリを使います。
以下の定義を profiles.clj に書くだけで全てのプロジェクトで使えるようになります。

:dependencies [[spyscope "0.1.5"]]
:injections [(require 'spyscope.core)]

するとこう書ける。

(defn foo [x]
  (filter even?
          #spy/p (map #(* % %)
                      (range x))))
(foo 10)
;(0 1 4 9 16 25 36 49 64 81)
;=> (0 4 16 36 64)

他にも #spy/p だと単純な print だけど、 #spy/d などもっとうるさいやつもあるので複雑なプログラムを書いているときはこちらのほうが役に立つこともあると思う。詳しくは README を。github.com
ただ、これと一緒に kibit-mode とか使っていると警告がいちいちうざかったりします。

Cider-debug を使う

先日紹介しましたが、 CIDER nREPL 0.9 以降で使える新しい機能のひとつですね。まだ 0.9 自体は SNAPSHOT なのであれですが :)ayato.hateblo.jp

定義ジャンプと REPL での評価

Cider で使えるテクニックです。
例えばこういう式があったとして

(println "Hello world")
;=> Hello world

println は組み込み関数のひとつですが、これ自身の挙動を知りたいとかちょっと書き換えてテストしたいという時があると思います。そういうときは M+. で定義元へジャンプします。
すると println の定義してあるファイルが Read-only なバッファで開かれます。 M-x M-q で Read-only を disable にして直接書き換えます。
この例だとこういう式が見えるはずなので、

(defn println
  "Same as print followed by (newline)"
  {:added "1.0"
   :static true}
  [& more]
    (binding [*print-readably* nil]
      (apply prn more)))

こう書き換えて

(defn println
  "Same as print followed by (newline)"
  {:added "1.0"
   :static true}
  [& more]
  (binding [*print-readably* nil]
    (apply prn (cons (str (first more) "!!")
                     (rest more)))))

この式を評価した後に、元の式を再評価すると

(println "Hello world")
;=> Hello world!!

となるわけですね。このやり方を知っておけばライブラリなどのデバッグをしたいとか Clojure それそのもののデバッグをしたいとかというときに便利です。ただ、 Java の定義に飛んでしまうとどうしようもないので諦めましょう…。

Checkout Dependencies を使う

上記よりもっと踏み込んでデバッグしたりとか開発したいとかいうときに使えます。例えば協調動作するような 2 つのライブラリを並行して書いているとか。
もちろん lein install して REPL をいちいち再起動するとかでも出来なくはないんですけど、それをやりたいかというと NO です*1

これは Leiningen の機能なので、特別に何かインストールする必要はありません。やり方として具体的にはこういうディレクトリ階層が普通あるんですけど、(仮に test というプロジェクトを作っています)

├── doc
├── resources
├── src
│   └── test
└── test
    └── test

そこに checkouts というディレクトリを追加して依存関係を持つライブラリに対してシンボリックリンクを貼ります。

├── checkouts
│   └── test-lib -> /home/ayato_p/projects/test-lib
├── doc
├── resources
├── src
│   └── test
└── test
    └── test

そしてそのまま REPL などを起動すれば一緒に checkouts 配下にセットした他のライブラリなどを読み込んでくれます。ここでは test-lib というものを checkouts に含めたわけですが、 test-lib の中身を変更した後に cider-refresh などをすることにより test-lib の変更を読み込みなおしてくれます。便利。

この機能を使うときに気をつけないといけないのは project.clj の dependencies を明示的に書かなくてもプロジェクトが動いてしまうことです。まぁ開発が一段落したら checkouts を消しても綺麗に動くか確認するのが無難でしょう。

今書いたことはチュートリアルにもしっかり書いてあるので、だいたい同じことですが読んでおくといいと思います*2
leiningen/TUTORIAL.md at master · technomancy/leiningen · GitHub

cider-refresh に関しては cider 0.8 以降であれば普通に使用可能なはずです。 C-c C-x!!
Emacs/CIDER で 5 倍快適 REPL リロードライフ - tnoda-clojure

Macro 展開する

Macro で書かれたものをデバッグするときはとりあえず展開して、展開されたものを眺めると挙動を理解しやすいです。
例えばこれは単純な例ですが、展開するとちょっとわかりやすいです。

(->> (iterate inc 0)
     (take-while #(< % 100))
     (map #(* % %))
     (filter even?)
     (drop-while #(<= % 100))
     count)

展開するとこう。

(count
  (drop-while
    #(<= % 100)
    (filter
      even?
      (map #(* % %) (take-while #(< % 100) (iterate inc 0))))))

まぁスレッディングマクロくらいなら別に何も難しくないですけど、マクロ展開は Cider-mode のコマンドひとつで簡単に出来るので覚えておいて損はないでしょう。
C-c C-m で macroexpand-1 相当、 C-c M-m で clojure.walk/macroexpand-all 相当となります。便利。

あとがき

最近、 Clojure の言語についてどうこうというより実用的なノウハウが欲しいなと思うことが多いです(できれば日本語で(読むのが楽なので))。なんというか言語機能的な話はよく出ますが、あまりこういう部分ってまだこなれてなかったりしてあまり話題になりにくいなあと感じています。 こういう話もどんどん出来たらいいですね :)
もし他にいい方法があればぜひコメント欄にでも書き込んでもらえると嬉しいです :)

*1:実際この方法教えてもらうまでやってましたけど…

*2:ここ読んでなかったから知らなかった…