(define -ayalog '())

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

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

*1:少なくとも Cursive は defxxxx マクロで定義した変数/関数にアクセス出来ない

*2:このままだと target ディレクトリまでタグ付けの対象になるので避けたければ .globalrc の skip オプションの一番後ろに target/ とか書いておくと良いです

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

*1:多分

*2:この順番がどういう風に実際決まっているのかはよく分からない

*3:多分!!

*4:as usual!!

*5:奇跡!!

*6:ヒュ~

*7:ちなみに僕の場合はなぜか wrap-anti-forgery 関数の引数がひとつしかない(つまり古いはず)のに、依存性で ring/ring-anti-forgery は別に競合していないというつらい感じの状態でハマってました。結局 ring-anti-forgery 時代の 0.2.1 と ring/ring-anti-forgery の 1.0.0 が入っているという状態で面白おかしくハマってました

あるディレクトリ以下のファイルを全てフォーマットし直したい

僕は普段 Emacs を使っている。しかし、最近 Cursive*1 を使うこともある。
それで何が困るかというと Cursive はちょっとイケてないので、ちゃんと Clojure(Script)? をフォーマットすることができない*2Clojure のスタイルガイドだと例えば with-* マクロのときは 2 スペース分インデントするとかあるんだけど、それを Cursive でよしなにするの結構めんどくさいというか、独自で with-* を定義してもそれをデフォルトで綺麗にインデントしてくれない。だからそのままコミットすると困ったことになるわけですね。

じゃあコミット前に自動でどうにかしたい、と思うわけです。賢明な読者なら既に気付いていると思うけど、そう Emacs Lisp を使えばいいんです。

スクリプト言語としての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$"

こう呼びだせばいい感じにしてくれると。これをコミットフックあたりで実行すると幸せになれそう。

(あるいはコミットフック使う前提なら、コミットする対象のファイルのパスを全部送ってフォーマットし直すようなスクリプトを書く方が効率よいですね)

*1:IntelliJ IDEA

*2:先日話した発表でも言及したけど

#渋谷Java 第十一回で Clojure 用の IDEA プラグインである Cursive の紹介してきた。

何をとち狂ったのかセッション枠という 20 分話せる枠をもらってしまったので話してきた。

元々は Clojure 入門みたいな話をしようと思ったんだけど、やっぱり入門以前にエディタとか IDE で「自分たちが普段使っているものが使えるのか」って大事だと思ったので今回は IntelliJ IDEA も使えるよって話をすることにした。
ちなみに何故 Eclipse じゃないのかと言われると、「 Clojure 使いが実際に使っていてシェアが高い」ことも同じくらい大事だと思ったので IDEA にした。あとはたまたま僕が IDEA のライセンスをたまたま持っていた*1からちょうど良いかってのもあった(もし、自分で試してみて納得行くなら Emacs から移ってもいいかなーと思ったしね)。

発表した内容

以下は発表したスライド

Youtube に実際に今日デモでやった内容と同じ内容をアップしているので興味ある人はどうぞ。

結局 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キーバインドが使いにくかったので諦めて普段の開発環境使ったというだけです。

余談 2

Scala 使ってる人で、「人類には早すぎた」って言ってる人 Clojure やりませんか?と思ったりした。

余談 3

ビズリーチ良いとこだった。

余談 4

次回は Clojure か ClojureScript の話を真面目にしにいきたい。
「ネタじゃない ClojureScript の話」とか。

残念だったこと


今日会えるのを楽しみにしていたので、残念でした。お大事に…。

*1:年に一度 Kotlin を書く時期に必要になる

*2:2 週間くらい開発のお供にしてた

*3:この機能はオフに出来る

*4:一度その機能をオフにして括弧の対応を揃えてから機能をオンにする必要がある

*5: Cursive 開発チームは Boot まだまだ issue 多いので、それ用の対応する気は今のところなく様子見ぽいです

*6:どちらにしても dev 用に watch タスクも一緒に走らせたいので都合がいい

*7:マテ

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 書いてる。