Clojure の開発で gtags を使う
えっと Emacs/Cider ユーザーにはあまり関係ないですね。あと他のエディタや IDE で定義ジャンプ出来る人はいらないと思う。
僕はこの前から inf-clojure を使っているので、これが欲しかった。あと例えば他のエディタや IDE で defxxxx なマクロを使っていると定義ジャンプ出来ないシーンがある気がしたので*1、どうせなのでそのへんまとめていい感じにしようかなと思った。
必要なもの
- Exuberant Ctags
- GNU GLOBAL v6.3.2 以降
以下の記事が詳しいので、参照してインストールしましょう。
gtags - GNU GLOBALの対応言語を大幅に増やすPygmentsパーサーを導入する - Qiita
ctags の設定
Exuberant Ctags は Clojure に対応していないので、このまま使うと定義タグの生成が出来ずに定義ジャンプが出来ない。シンボルタグだけは上記の記事の通りにやれば生成出来るのだけど、検索対象が山のようになるだけで使い勝手悪いどころの騒ぎではないので、定義タグを作れるようにする。
~/.ctags を作って以下の設定を書く。
--langdef=Clojure --langmap=Clojure:.clj --langmap=Clojure:+.cljs --langmap=Clojure:+.cljx --regex-clojure=/\([ \t]*create-ns[ \t]+([-[:alnum:]*+!_:\/.?]+)/\1/n,namespace/ --regex-clojure=/\([ \t]*def[ \t]+([-[:alnum:]*+!_:\/.?]+)/\1/d,definition/ --regex-clojure=/\([ \t]*def[a-z]*[ \t]+([-[:alnum:]*+!_:\/.?]+)/\1/d,definition/ --regex-clojure=/\([ \t]*defn[ \t]+([-[:alnum:]*+!_:\/.?]+)/\1/f,function/ --regex-clojure=/\([ \t]*defn-[ \t]+([-[:alnum:]*+!_:\/.?]+)/\1/p,private function/ --regex-clojure=/\([ \t]*defmacro[ \t]+([-[:alnum:]*+!_:\/.?]+)/\1/m,macro/ --regex-clojure=/\([ \t]*definline[ \t]+([-[:alnum:]*+!_:\/.?]+)/\1/i,inline/ --regex-clojure=/\([ \t]*defmulti[ \t]+([-[:alnum:]*+!_:\/.?]+)/\1/a,multimethod definition/ --regex-clojure=/\([ \t]*defmethod[ \t]+([-[:alnum:]*+!_:\/.?]+)/\1/b,multimethod instance/ --regex-clojure=/\([ \t]*defonce[ \t]+([-[:alnum:]*+!_:\/.?]+)/\1/c,definition (once)/ --regex-clojure=/\([ \t]*defstruct[ \t]+([-[:alnum:]*+!_:\/.?]+)/\1/s,struct/ --regex-clojure=/\([ \t]*intern[ \t]+([-[:alnum:]*+!_:\/.?]+)/\1/v,intern/ --regex-clojure=/\([ \t]*ns[ \t]+([-[:alnum:]*+!_:\/.?]+)/\1/n,namespace/
元ネタは以下の設定だけど、元のやつだと defxxxx マクロで定義したシンボルを定義タグとして作成してくれないので少しだけ細工した。
my .ctags(exuberant-ctags) for Clojure
gtags の設定
以下の記事では Lisp の設定を乗っ取る形で実現しているけど、 GLOBAL の v3.6.2 以降だとデフォルトで Clojure サポートがあるはずなのでそちらを修正するようにする。
gtagsでClojureのコードのタグ付け - iwasakimsの日記
$ cp /usr/local/share/gtags/gtags.conf ~/.globalrc
とコピーしたら .globalrc を以下のように修正する
$ diff /usr/local/share/gtags/gtags.conf ~/.globalrc 28c28 < :tc=native: --- > :tc=native:tc=pygments: 170c170 < :langmap=Clojure\:.clj:\ --- > :langmap=Clojure\:.clj.cljs.cljx:\
ふたつ変更点があって、ひとつは default でも pygments を使う設定、ふたつめは cljs/cljx という拡張子に対応するため。
これだけ設定したら後はプロジェクトルートで gtags と実行するだけでいいわけです*2。
おまけ
Emacs ユーザーなら helm-gtags.el を使って幸せになりましょう。
syohex/emacs-helm-gtags · GitHub
むしろこれがあるから gtags を使えるようにしようと思ったくらい。
余談
Exuberant Ctags の後続プロジェクトというか fork した(?)プロジェクトで Universal Ctags というのがあるらしくて、それはデフォルトで Clojure をサポートしているんだけどパーサーが適当すぎて ns, defn, quote しかサポートしてないという。
これを使った場合でも同様に .ctags に設定書けば解決するんだけど、まぁ結局同じような内容書くならどっちでもいいよねという感じはある。
universal-ctags/ctags · GitHub
参考
Clojure で依存関係の解決に潜むワナ?
お仕事中に盛大にハマって、最終的に一緒にやってる Clojure チョットデキルマンに助けてもらった。
Clojure のライブラリを Leiningen や Boot に含める場合、次のような表記を用いる。
[group-id/name version]
Leiningen や Boot はこれを group-id/name でひとつのライブラリとみなし、もし数あるライブラリの内部で使われているライブラリが重複した場合には、後勝ちで依存性が解決されていく*1。
なので例えば foo/piyo というライブラリが a/nyan 0.9, a/nyun 0.8 というライブラリを依存関係に含んでいて、 bar/puyo というライブラリが a/nyan 0.1, a/nyun 1.2 というライブラリを依存関係に含んでいる場合に、 foo/piyo -> bar/puyo という順番で依存性が解決される*2とすると bar/puyo の中で使われているライブラリのバージョンが使われることになる。つまり a/nyan 0.1, a/nyun 1.2 になると思う*3。
このとき単純にバージョンが競合しているだけならまだ救いがある*4。 lein deps :tree を使えばどのライブラリからどういう依存があって、競合しているかが一目で分かる。 Boot の場合は boot show -p で競合をどう解決したかが確認出来る(単純な依存性グラフは boot show -d で確認出来ます)。
上述した通り、 Leiningen も Boot も group-id/name というのをひとつのライブラリとみなす。つまり、どういうことかというと、既に気付いた人もいると思うけど、もし「同じライブラリなのに名前が違うとコンフリクトしない」んです。そんなことあるわけがない、って思うかもしれませんが極稀に起こります。例えば ring-anti-forgery 。 ring シリーズのひとつとして group-id に ring をつけるようになったのは 0.3.1 以降です。そして、 0.3.0 以前のものを依存性に持っているライブラリが幾らか存在したりします。そうするとこの問題にあたる可能性が多少あるわけですね。幸運にも依存性の解決の順番が ring-anti-forgery -> ring/ring-anti-forgery だった場合、何も起こりません*5。
更に厄介なのが lein ancient のようなツールを使ったとしても、更新を発見することが出来ないため上の例の場合だと ring-anti-forgery 0.3.0 で止まるんですね*6。恐らくライブラリ開発者などで「 lein ancient を使ってるから依存ライブラリは全て最新だ、うぇーい」みたいなことをやっていると、こういう風になってしまうんでしょうね…。
とまぁ、そんなこんなで、この問題だけはツールで解決する方法が恐らくないので、ハマったときの為に頭の片隅にでも入れておくと良いのではないでしょうか*7。
あるディレクトリ以下のファイルを全てフォーマットし直したい
僕は普段 Emacs を使っている。しかし、最近 Cursive*1 を使うこともある。
それで何が困るかというと Cursive はちょっとイケてないので、ちゃんと Clojure(Script)? をフォーマットすることができない*2。 Clojure のスタイルガイドだと例えば with-* マクロのときは 2 スペース分インデントするとかあるんだけど、それを Cursive でよしなにするの結構めんどくさいというか、独自で with-* を定義してもそれをデフォルトで綺麗にインデントしてくれない。だからそのままコミットすると困ったことになるわけですね。
じゃあコミット前に自動でどうにかしたい、と思うわけです。賢明な読者なら既に気付いていると思うけど、そう Emacs Lisp を使えばいいんです。
ということでこんな感じのスクリプトを書いた。
;; 重くなるけど、 Emacs の初期化ファイルがないとそもそも Clojure モードとかない( script 実行用の初期化ファイルをある程度軽量化版で作れば問題なさそうだけど、時間の都合で割愛) (load (locate-user-emacs-file "init")) (defun directory-files-recursive (direcotry match) (let* ((files '()) (current-directory-list (directory-files direcotry t))) (while current-directory-list (let ((f (car current-directory-list))) (cond ((and (file-regular-p f) (file-readable-p f) (string-match match f)) (setq files (cons f files))) ((and (file-directory-p f) (file-readable-p f) (not (string-equal ".." (substring f -2))) (not (string-equal "." (substring f -1)))) (setq files (append files (directory-files-recursive f match)))) (t))) (setq current-directory-list (cdr current-directory-list))) files)) (defun re-format-file (file) (find-file file) ;; だいたい拡張子に紐付いたモードに自動的になるはずなので問題ないけど、必要があればこの位置に (clojure-mode) とか入れたらいいと思う (indent-region (point-min) (point-max)) (save-buffer)) (let* ((dir-name (car argv)) (match (cadr argv)) (matched-files (directory-files-recursive (car argv) (cadr argv)))) (mapcar 're-format-file matched-files))
これを ~/.emacs.d/scripts とかディレクトリ適当に作ってその中に保存しておいて
emacs -Q --script ~/.emacs.d/script/re-format.el ~/some/clojure/proj/dir "\\.clj$"
こう呼びだせばいい感じにしてくれると。これをコミットフックあたりで実行すると幸せになれそう。
(あるいはコミットフック使う前提なら、コミットする対象のファイルのパスを全部送ってフォーマットし直すようなスクリプトを書く方が効率よいですね)
参考
#渋谷Java 第十一回で Clojure 用の IDEA プラグインである Cursive の紹介してきた。
何をとち狂ったのかセッション枠という 20 分話せる枠をもらってしまったので話してきた。
元々は Clojure 入門みたいな話をしようと思ったんだけど、やっぱり入門以前にエディタとか IDE で「自分たちが普段使っているものが使えるのか」って大事だと思ったので今回は IntelliJ IDEA も使えるよって話をすることにした。
ちなみに何故 Eclipse じゃないのかと言われると、「 Clojure 使いが実際に使っていてシェアが高い」ことも同じくらい大事だと思ったので IDEA にした。あとはたまたま僕が IDEA のライセンスをたまたま持っていた*1からちょうど良いかってのもあった(もし、自分で試してみて納得行くなら Emacs から移ってもいいかなーと思ったしね)。
結局 Cursive ってどうなの?
スライドの最後に書いてある通りですが、 Emacs 使い的にはまだ「ちょっと微妙」です。ただ、今のところ一番夢見れる IDE なんじゃないですかね。 Light Table や Nightcode は普段から使ってないからなんとも言えないですけど、個人的に Cursive は「かなり、あり」です。あとは IntelliJ IDEA を普段使いしている、もしくは関連 IDE を使っている人なら簡単に使えるはずです。ちょっとマクロ展開ができたりできなかったりするお茶目な不具合とかもわりとヘビーに使った*2ので知ってますが、そのうち直ると思うし期待してます。
たぶん Emacs ユーザー、特に Cider ユーザーにとっては全然機能が足りないというか、ライブラリ開発者とかだとちょっと足りないと思うと思います。 Emacs なら簡単に定義元ジャンプが出来てなおかつ、読み取り専用バッファを書き込み出来るようにして修正して Clojure のコードを評価とか当たり前にやると出来るんですが、 Cursive では定義元ジャンプは出来ても修正して評価が出来ないです。そこが Cursive 使ってて、あーって思ったところですかね。勿論コピーして REPL の中で名前空間変えて評価するっていう方法もあるけど、 Emacs の方が簡単ですね。
それとなんだろう defxxx 系のマクロで定義されている変数/関数は「未定義」扱いされるので、「未定義だよー」ってハイライトされちゃいます*3。この未定義扱いは地味に不便で確かこれは定義元ジャンプ出来なかった気がする。 Paredit 相当の構造的編集機能めっちゃ便利だけど、強制的に括弧を消したり足したり出来ない気がするので、何かの拍子に括弧の対応が崩れるとそれを修正するのがめんどくさい*4。
勿論逆に Emacs ユーザーから見た「あ、この機能いいなぁ」というのもちょくちょくあって、 Java のコードに到達したときにソースがなくてもデコンパイルした結果が見れるとか、ソースをダウンロードしてそっちを代わりに表示出来るとかいいなぁと。あと、特別な設定しなくても補完がほぼ完璧に効くのは良かった。デバッグ機能もちらっとしか触ってないので評価しにくいですけど、昔 Java 書いてたときみたいにグラフィカルに触れるのはいいなぁと。
Remote REPL に接続して開発出来るので今のところ Boot 使っている場合はそっちで対応する必要があります*5。まぁ仕事で使ってますが、あまり困ってないですね*6。
まとめると Emacs の自由さは最強だと思うんだけど、 IDEA の簡単に色々出来ちゃう便利さはそれはそれでとてもいいので、このふたつを組み合わせた何かがあったらいいのになー*7。
反省点
割と IntelliJ とか Clojure とか知っている体で話してしまったのはまずかったかなーと後で反省。あと、ちょっとテンパってデモの中で文字を大きくするの忘れてた。話し方も下手なのでもう少しちゃんと落ち着いて話したい。
余談
スライド中の IDEA のスクリーンショット途中から Mac のインターフェイスから StumpWM なインターフェイスになってますが、理由はあまりにも Mac のキーバインドが使いにくかったので諦めて普段の開発環境使ったというだけです。
余談 3
ビズリーチ良いとこだった。
余談 4
次回は Clojure か ClojureScript の話を真面目にしにいきたい。
「ネタじゃない ClojureScript の話」とか。
残念だったこと
あ、ありのまま(ry
地味に治らない捻挫だと思って病院に来たら足首を固定され松葉杖の使い方をやさしく教えられている…
— Kazuhiro Serizawa (@seri_k) May 30, 2015
今日会えるのを楽しみにしていたので、残念でした。お大事に…。
ClojureScript の中で JSON を Clojure のデータ形式にするときにマップのキーをキーワードにする
メモ。
JSON オブジェクトを受け取って js->clj に突っ込むだけだと、 JSON のキーがそのまま文字列で来るので :keywordize-keys オプションをつけてあげる必要がある。
(ns example.core (:import [goog.net XhrIo])) (defn callback [reply] (let [v (js->clj (.getResponseJson (.-target reply)) :keywordize-keys true)] (.log js/console (:a v)))) (.send XhrIo "/any-action" callback)
余談: JS 非同期脳だったので core.async のパラダイム理解するの難しいと思いながら cljs 書いてる。