UZABASE Tech Blog

〜迷ったら挑戦する道を選ぶ〜 株式会社ユーザベースの技術チームブログです。

Clojure 1.10.x時代の新しいデバッグツール

こんにちは!こんにちは!SPEEDA開発チーム(通称PDT)に所属しているあやぴーid:ayato0211です。

弊チームでは新しいモノを開発するときに、よくClojureという言語が採用されています。だいたい言語別でシェア2位といったところでしょうか。1位はみんな大好き(?)Kotlinで、こちらはサーバーサイドでの利用が主になっていて、次にE2Eのテストコードを書くときに良く利用されています。

今日はそんな弊チームでよく使われているClojureについて、Clojure 1.10.x時代の新しいデバッグツールをお伝えできれば良いなと思い、この記事を書こうと思った次第です。

大きなデータ構造を簡単に把握したい

Clojureは素晴らしい言語です。Clojureはとても素晴らしい言語です。大事なことなので、2回言いました。それはさておき、Clojureでアプリケーションを書いていると、どうしても大きなデータ構造が出てくる瞬間があります。例えば、Ringのリクエストマップや、Ductのコンフィグマップなどが該当します。それ以外にもマップを引数に取る関数を書くことは往々にしてあるかと思います。

これらの大きなデータ構造というのは、実際に中身を確認するのが非常に面倒です。まず思いつくのは、伝統的なプリントデバッグだと思います。このとき、よくあるのはそのマップがどんなキーを持っているのか分からないので、①とりあえずマップ全体を出力してみて、②注目したい目的のキーをなんとか探し出し、③目的のキーと対応する値だけを出力するように修正して④再度確認を行うという①~④の繰り返しをすることです。

この伝統的な方法はデータ構造が大きければ大きいほど目当ての値を見つけるのが難しいです。僕はよく出力した後に迷子になっています。できればもっとインタラクティブに出力したデータ構造を把握しやすい方法が欲しいところです。そういうツールを今回は紹介したいと思います。

Cognitect's REBLを使う

インタラクティブにデータ構造を把握するためのツールとして、オススメしたいのがCognitectが開発しているREBLというツールです。

REBL、つまりRead Eval Browse Loopということで、その名前の通りただ標準出力に吐く代わりに値をブラウジング(走査)できるというわけです。REBLを使うために必要なのはClojureのバージョンが1.10.0以上であることです。Clojure 1.10.0で新しく追加されたtap>を使うと、REBL上に任意のデータを送ることができ、簡単にブラウジングすることができます(より厳密に言えばDatafyなどの話も出てくるんですが一旦脇に置いておく)。

このREBLをどうやって既存プロジェクトで利用できるようにするのか、Ductの簡単なアプリに組み込む例を見せながら解説したいと思います。

REBLのダウンロードとインストール

REBLは以下のページからダウンロードすることができます。

ライセンス条項をよく読んでほしいのですが、個人の趣味として利用する場合にはあまり問題ありませんが、仕事で利用する場合はDatomicのアクティブな有償ユーザーもしくは、REBLのパトロンでなければなりません。このことに注意してダウンロードしてください。

zipファイルをダウンロードしたら適当な場所に展開しておきます。今回は/home/{{username}}/opt/REBL-0.9.172/REBL-0.9.172.jarとなるようにしました。これでREBLのインストールは完了です。

プロジェクトの準備

次にREBLを使う対象のプロジェクトを準備します。今回はDuctのAPIを簡単に作成して、ついでにexampleハンドラーも生やしてしまいます。以下のコマンドでプロジェクトを作成します。

$ lein new duct demo --template-version 0.12.1 -- +api +ataraxy +example
$ cd demo
$ lein duct setup

普段、意識して--template-versionを指定することはありませんが、この例と全く同じ状況を作りたければ、このように指定することで同じ雛形のバージョンを利用できます。

REBLを利用できるようにする

まずは/home/{{username}}/.lein/profiles.cljに以下の情報を追記します。REBLは開発時に使うもので、開発環境(Javaのバージョン)によって必要となる依存関係が微妙に異なるので、開発者のマシン毎に以下の設定はあると良いでしょう(REBLのREADMEに書いてあるUsageを参照)。僕の手元の環境が今はJava 11になっているのでJava FX関連の依存関係が入っています。また先程インストールしたREBLのJarのパスを:resource-pathsに記述します。

{
 ;; 以下の部分を書き加える
 :tools/rebl
 {:dependencies [[org.clojure/core.async "0.4.500"]
                 [org.openjfx/javafx-fxml "11.0.1"]
                 [org.openjfx/javafx-controls "11.0.1"]
                 [org.openjfx/javafx-swing "11.0.1"]
                 [org.openjfx/javafx-base "11.0.1"]
                 [org.openjfx/javafx-web "11.0.1"]]
  :resource-paths ["/home/{{username}}/opt/REBL-0.9.172/REBL-0.9.172.jar"]}}

次に先程用意したプロジェクトのプロジェクトルート以下にあるprofiles.cljを以下のように書き換えます。profiles.clj.gitignoreされるので、個人用の設定を書く場所として適しています。

;; Local profile overrides
{:profiles/dev
 [:tools/rebl]}

最後にdev/src/local.cljを以下のように記述します。このように書くと、Ductのプロジェクトで最初に(dev)とするとこのファイルが自動的にロードされ、REBLの画面を立ち上げる関数(start-rebl)が使えるようになります。開発しているプロジェクトでチーム全員の合意がとれる場合は、dev/src/dev.cljなどに直接REBLを起動させるための関数を書いても良いかもしれません。

;; Local REPL configuration
(when-not (try
            (require 'cognitect.rebl)
            (catch Exception _))
  (defn start-rebl []
    ((resolve 'cognitect.rebl/ui))
    (add-tap clojure.pprint/pprint)
    (println "REBL started")))

ここまででREBLを利用する下準備が整いました。

REBLにデータを送信する

まずは普段どおりにREPLからシステムを起動をして、REBLもあわせて起動させておきます。

user> (dev)
;;=> :loaded
dev> (go)
:duct.server.http.jetty/starting-server {:port 3000}
;;=> :initiated
dev> (start-rebl)
REBL started
;;=> nil

このまま例えばターミナルなどからcurl localhost:3000/exampleとしても、REBLの方には何も出力されません。なので、src/demo/handler/example.clj:demo.handler/exampleコンポーネントを次のように少し書き換えます。

(defmethod ig/init-key :demo.handler/example [_ options]
  (fn [{[_] :ataraxy/result :as req}]
    (tap> req) ;; <- 追記
    [::response/ok {:example "data"}]))

リクエストマップをtap>に渡すようにしました。こうして、REPLなどからシステムを(reset)などを実行して再起動します。そして、先程と同様にcurl localhost:3000/exampleとターミナルから実行してみます。するとREBLの「tap」タブに次のようにリクエストマップが出力されていると思います。

f:id:ayato0211:20190702182751p:plain

この「tap」タブの画面下部にある「Browse」ボタンを押すと「browse」タブに表示が切り替わります。「nav->」入力欄に例えば0 :headersと入力してEnterを押してみましょう。すると次のような画面になっているはずです。nav->のところはget-inと同じ要領で書けると思えば問題ないと思います。

f:id:ayato0211:20190702182849p:plain

今までであれば出力したデータを見ることしかできませんでしたが、REBLを使えばこのように一度出力した値をREBL上で走査することができます。これにより今までより大きなデータ構造などを把握しやすくなり、デバッグなどもやりやすくなるはずです。

REBLのより具体的な使い方は実際に触るか、次の動画を見てもらえると良いかなと思います。

まとめ

今までプリントデバッグでprintlnpprintなどと書いていたところを、tap>と書くだけでREBLにデータの情報を送ることができ、REBL上でインタラクティブにデータを見ることができるようになりました。これにより、Clojureで開発されたアプリケーションをよりデバッグしやすくなったりするのではないでしょうか。

余談

今回は説明していませんが、Clojure 1.10.0で追加されたdatafyなどをうまく活用すると、REBL上で走査できる対象を広げることができます。実際、ネームスペース(clojure.lang.Namespace)などはDatafiableを実装しているため、REBL上でブラウジングすることができるようになっています。REBL左上のパネルに(the-ns 'dev)など入力して評価してみると分かると思います。このあたりの話もいずれ書けたら良いなーと思ったり思わなかったり。