本当は怖い情報科学

情報系大学院生の趣味&実益ブログ。Clojure、Javascript、Pythonその他。

Clojure Debugging ’13: Emacs, nREPL, and Ritz

この記事は、Clojure Debugging ’13: Emacs, nREPL, and Ritzの翻訳です。著者の許諾をいただいています。また、Qiita上でのClojure Advent Calendar 2013参加記事(12/12)です。

細かいところの動作確認・アップデートの有無の調査等はまだできていませんのでご了承ください。そのうちやります。翻訳の更新履歴は末尾に有ります。

                                                                                                              • -

私は今、2013年と2014年の開発プロジェクトの準備を進めているところだ。2010年時代のswank-clojureとslimeを使ったアプローチは、もはやこのプロジェクトで使い続けることはできないだろう。nREPLのコミュニティに参加して、自分が働く分散システムのデバッグを容易にする技術革新の恩恵を受けるのだ。

私がcommon lispとslime/swankで 慣れ親しんでいた機能は:

  • Meta-. と Meta-, によるコード移動
  • エディタ・ウィンドウとreplにおける曖昧補完
  • mini-bufferでのドキュメント参照
  • オブジェクト・インスペクタ。システム中の任意の値を調査できる機能
  • 問題箇所へ1キーで飛べるバックトレース
  • 特定のフレームで値を評価し、値をインスペクトできる
  • replおよびtrace bufferでの、使いやすいコードトレーシング
  • ウォッチポイントやブレークポイントを使って、継続可能なバックトレース表示

だ。普通のnreplでは、最初の3つの機能しか使えない。この記事では、2013年5月時点で提供されているnREPLミドルウェアemacsを使って、残りの機能に近いものを構築する方法について書くことにする。

nREPLは,エディタなどのクライアントから、動作中のClojureインスタンス(サーバー)に接続して、コードナビゲーション・シンボル補完、動的なコード評価などの機能を利用できるようにするフレームワークだ。slime/swankと比較すると、nREPLは、通信方法から独立で、コマンドと返答のmapを非同期にやりとりすることに焦点が置かれている。それに加えて、基礎的な通信の定義の上にREPL機能を実装するための最低限のミドルウェアも作られている。

基本的な設定方法

それでは、Emacsのnrepl.elを使って基本的なnREPLを動かして、うまい具合にデフォルト値を設定しよう。

まず、Emacs24をクリーンインストールする。
~/.emacs.d/init.el を変更して、ELPAがMELPAを使うように設定しよう。

(require 'package)

(add-to-list 'package-archives
'("melpa" . "http://melpa.milkbox.net/packages/") t)
(package-initialize)

Emacsを再起動して、M-x package-list-packages を入力し、ac-nrepl, clojure-mode, melpa, nrepl, nrepl-ritz, rainbow-delimiters をインストールしよう。
次に、これらのパッケージを設定。

(require 'nrepl)

;; Configure nrepl.el
(setq nrepl-hide-special-buffers t)
(setq nrepl-popup-stacktraces-in-repl t)
(setq nrepl-history-file "~/.emacs.d/nrepl-history")

;; Some default eldoc facilities
(add-hook 'nrepl-connected-hook
(defun pnh-clojure-mode-eldoc-hook ()
(add-hook 'clojure-mode-hook 'turn-on-eldoc-mode)
(add-hook 'nrepl-interaction-mode-hook 'nrepl-turn-on-eldoc-mode)
(nrepl-enable-on-existing-clojure-buffers)))

;; Repl mode hook
(add-hook 'nrepl-mode-hook 'subword-mode)

;; Auto completion for NREPL
(require 'ac-nrepl)
(eval-after-load "auto-complete"
'(add-to-list 'ac-modes 'nrepl-mode))
(add-hook 'nrepl-mode-hook 'ac-nrepl-setup)

Leiningenのバージョンが2.0.0以降であることを確かめて、ネットワーク経由でのreplを起動するためにlein replと打ち、コマンドラインから接続してみる。’nREPL server started on port xxxxx’と表示される。そしたら、EmacsからM-x nreplと打って、次に 127.0.0.1 、ポート番号であるxxxxxを入力しよう。

ここまでの基本設定で、便利な機能が沢山使えるようになっている。

  • 現在のClojureインスタンス上でREPLベースの対話実行
  • M-.M-, で移動
  • エラーで簡潔なバックトレース表示
  • C-c C-kでファイルをロード
  • C-c C-dで現在のシンボルをdescribe
  • だいたいどこでもイケてる補完ができる
  • 現在の表現の先頭で、mini-bufferにAuto-doc helpを表示
  • C-c C-zで現在のnREPLへ移動

シンプルなオブジェクト・インスペクタ

でも、これでは十分に満足とはいえない。’C-c C-i’で基本的なオブジェクト・インスペクタを使えるようにしよう。ライブラリがきちんとパッケージ化されるのを待っていてもいいのだけれど、新しいミドルウェアClojureサーバーとEmacsクライアントで使えるようにするいい例だ。

git clone https://github.com/vitalreactor/nrepl-inspect.git
cd nrepl-inspect
lein install

init.el の(require ‘nrepl) の次の部分のを更新しよう:

(load-file "/path/to/nrepl-inspect/nrepl-inspect.el")
(define-key nrepl-mode-map (kbd "C-c C-i") 'nrepl-inspect)

で、 ~/.lein/profiles.clj を設定:

{:user {:dependencies [[nrepl-inspect "0.3.0”]]
:repl-options {:nrepl-middleware
[inspector.middleware/wrap-inspect]}}}

nreplとemacsを再起動して、replへ再接続する。

=> (def testing {:foo :bar})
=> testing

C-c C-i と打つと、ミニバッファで値を入力するためのプロンプトが出る。値だけでなく、現在のnreplかエディタの名前空間で評価される任意の式を入力することができる。Tab, Shift-TabそしてEnterキーで構造の一部だけを見ることができる。’l’で前の値で戻り、’g’で値を更新できる(もしatomagentみたいな物を見ているなら)。現在のところ、長いシーケンスを省略したりはしないから、無限シーケンスや巨大なマップには気をつけること。


[NOTE: 最初に記事を書いた後、僕は値を見るためのTechnomancyのjavertライブラリと拡張を書き直して、上記の部分をそれを使うように書き直した。]

Ritzを使う

Hugo Duncanは、nrepl用の非常に高機能ですばらしいミドルウェアを作るすばらしい仕事をした。元々はこのコードはswank-clojureから派生しているからswankプロトコルもサポートしているけど、今となってはこれは使わないのが得策だ。

簡単なところから始めよう。

M-x package-list-packages
Install nrepl-ritz

init.elを更新しよう。

(require 'nrepl-ritz) ;; after (require 'nrepl)

;; Ritz middleware
(define-key nrepl-interaction-mode-map (kbd "C-c C-j") 'nrepl-javadoc)
(define-key nrepl-mode-map (kbd "C-c C-j") 'nrepl-javadoc)
(define-key nrepl-interaction-mode-map (kbd "C-c C-a") 'nrepl-apropos)
(define-key nrepl-mode-map (kbd "C-c C-a") 'nrepl-apropos)

profile.cljも、サーバーミドルウェアを使えるように更新する。

{:user {:plugins [[lein-ritz "0.7.0"]]
:dependencies [[nrepl-inspect "0.3.0"]
[ritz/ritz-nrepl-middleware "0.7.0"]]
:repl-options
{:nrepl-middleware
[inspector.middleware/wrap-inspect
ritz.nrepl.middleware.javadoc/wrap-javadoc
ritz.nrepl.middleware.apropos/wrap-apropos]}}}

これで、nrepl.elで新しいコマンドが2つ使えるようになる:nrepl-javadocnrepl-aproposだ。上のように、これらを適当なキーにバインドできる。

‘doc’とか、いくつかのミドルウェアはtools.nreplに直接移植されたから、ritzのミドルウェアは、今となってはデフォルトの配布物では冗長だ。Ritzはnrepl.elもしくはtools.nreplには含まれていないパッケージをいくつか持っているけど、僕はまだ試していない。プロジェクト用のミドルウェア(再起動無しでのプロジェクトの更新・切り替え)、codeqデータベースへの接続、source form trackingが含まれている。
(訳註:source form trackingってなんだろう?)

Ritz Debugger

最初にリストアップしたものに欠けている大きな物は、対話的なバックトレース、ブレークポイント、ウォッチポイントとトレースだ。Ritzデバッガは、最後のもの以外はサポートしているが使うのはちょっと面倒くさい。

必要な物はすでにprofile.cljとinit.elに含まれている。デバッガの機能を有効にするために必要なのは、leiningenのproject.cljを開いて M-x nrepl-ritz-jack-inを呼ぶだけだ。これは2つのプロセスを立ち上げて、デバッガのセッションを開始する。

jack-inすれば、M-x nrepl-ritz-break-on-exceptionで対話的なバックトレースが使える。バックトレースから抜けるときは、’q’じゃなくて’1’を入力する。僕の環境では、’q’を入力するとフリーズしてしまう。M-pM-nで、スタックフレームを行ったり来たりできるし、’t’でローカル変数を見たり、’v’で該当のスタックフレームのソースへ移動したり、’e’特定のスタックフレームで値を評価したりできる('C-g m’で他全部のオプションが表示される)。

C-c C-x C-bで対話的ななスタックフレームに突入するブレークポイントを設定することもできる。nrepl-ritz-breakpointsバッファは0.7.0の時点では壊れてるみたいだけど。

最後に、M-x nrepl-ritz-threads でスレッドの一覧を見ることができる。

lein-ritzパッケージは新しいleinコマンドをサポートしている:lein ritz でJVMプロセスが2つ起動する。1つめは標準のシステムで、JPDA/JDIを使ったデバッガプロセスだ(これについては、Hugoのプレゼン資料を参照のこと。)

現在のところ、M-x nrepl を使ってプロセスへアタッチする機能はうまく動いていない。

リモートデバッグ

リモートで動いているシステムでオブジェクトインスペクトやデバッグの機能を使うにはどうすれば良いだろうか?現在のところは、nreplの基本的な機能といくつかの選ばれたミドルウェアだけを使って、ritzがもっと安定するまで避けることをおすすめする。セットアップは複雑だし、僕もまだ全部理解していない。もしあなたが理解できたら、下のコメント欄にコメントしてください。この部分を直すので。

このミドルウェアを使うためにはproject.cljをtools.nreplを使うように変更して、依存するパッケージも全て含まれるようにする必要がある。そうしたら、下のようにミドルウェア付きのサーバーを立ち上げることができる。

(use '[clojure.tools.nrepl.server :only (start-server stop-server default-handler)]
(use '[inspector.middleware :only (wrap-inspect)])

(defonce server
(start-server
:port 4006
:handler (default-handler #'wrap-inspect)))

[NOTE: 最初に記事を公開した後、ミドルウェアをrequireしてサーバー起動時に渡す必要があることに気がついた。以前はこの部分が欠けていた。]

デフォルトではlocalhostしかlistenしないから、EmacsアプリケーションサーバーのあいだにSSHトンネルを張るのが良いだろう。もしPaaSを使っているなら、それぞれの専用のやりかたがあるだろう。ClojureコミュニティはHerokuのヘビーユーザーだから、Heroku用にはうまい方法がある。

nreplの大きな利点は、nREPLサーバーとクライアント間の通信に複数の通信経路を使うことができることだ。HTTP/HTTPSとritz-hometqは良い例だ。リモートデバッグについては、数ヶ月後に、新しいプロジェクトが落ち着いたら書くことにする。ImmutantチームがRitzにリモートでバッグの機能を追加するために作業しているようだ。

まとめ

Clojure向けのSlime/Swankは、急速に忘れ去られつつある。今日明日にすぐに乗り換える必要は無いとはいえ、Clojureの開発者の意識はnREPLに向いているから、古い環境向けにはパッケージはメンテされないだろう。乗り換えは避けられない。幸いなことに、nREPLは急速に成熟しているし、不足している部分はRitzのようなプロジェクトが埋めてくれるのだ。

                                                                                          • -

更新履歴

  • 12月13日:微修正