Boot を Leiningen プロジェクトの中で使う
一緒にお仕事している Clojure デキルマンに Boot を闇ツール扱いされる今日この頃です。
日本ではあまり使っている人が多くない*1 Boot ですが、あやぴー的にはかなり気に入っています。ちなみに先日バージョン 2.0.0 がリリースされています。
まぁ、 Boot いいよ、とは言うものの正直 Leiningen で満足しているし、何も不便ないよという人たちも多いかもしれません。しかしながら、 Leiningen がもう少し柔軟にタスクのようなものを定義できたら…とか思うこともたまにはあると思います。 Boot はそういうことに長けていますが、それだけのために今の Leiningen のプロジェクト設定を全て捨てて Boot を使いはじめるというのはなかなかの苦痛だと思います。なので、今日紹介するのは Leiningen のプロジェクトをベースに Boot のタスクを使えるようにするという方法です。
Leiningen の project.clj が単純なマップであることは言うまでもないですが、であれば簡単に Clojure で読み込んでそのデータをそのまま使えるということは想像に難くないと思います。そして Boot の build.boot には Clojure のコードを普通に書けます。言いたいことは分かりますよね?
次のようなコードを build.boot の中で書くと簡単に project.clj の依存性などを Boot から参照して使うことができます。
(def lein-proj (->> "project.clj" slurp read-string (drop 3) (partition 2) (map vec) (into {}))) (set-env! :source-paths (into #{} (:source-paths lein-proj)) :resource-paths (into #{} (:resource-paths lein-proj)) :dependencies (into [] (:dependencies lein-proj)))
弊社プロジェクト内での使い方はもう少し違いますが、これをベースにした考え方で共存させています。
Boot is 闇?
たまにこの共存をしているがために起こる問題があります。だいたいは Leiningen と Boot の依存性の解消方法が違うためです。例えば midje を使っていて、使われる slingshot のバージョンが違うからテストが通らないという事件があったり…。
こういう場合は lein deps :tree と boot show -p を使ったりしてどのバージョンが使われているかを確認したりする必要があります。まぁ謎のエラーが起こったらだいたい依存性周りで変なバージョンが入っている可能性があるので、確認すれば案外素直に解決できるかと思います。
Clojure を始めたいけど、周りに Clojure 使ってる人いないし聞ける人いないからなーと思っている人たちへ
「 Clojure が素晴らしい言語であるのはなんとなくわかった。興味はあるんだけど、何処に実際に使っている人たちいるの?」とか「ちょっと分からないところがあるから聞いてみたいんだけど、 QA サイトに投稿するほどじゃないんだよなー」という人たちが一定数きっといると思うんだけど*1、そういう人たちは Slack の Clojure 使いが集まる場所に是非きてください。
以下のページから登録できます(ちょっと時間かかるけど、一日以内でだいたい招待されると思う)。
ここに #clojure-japan というチャンネルがあり、そこには日本語の通じる Clojure を日々業務で使っていたりする人が多くいます*2。
勿論、 #clojure, #clojurescript, #cider などといった特定のトピックに対応したチャンネルもあるので興味ある人は覗いてみてもいいかと思います(ローカルチャンネル以外は英語ですが…)。
日本の Clojure 界隈まだまだこれからなので、新しい人増えると僕が嬉しいです :)
メモ
ログはこっちから全部見れますよ。
Clojurians Slack Log
Clojure の開発環境をよりシンプルへ ~ inf-clojure 実践的な設定編?
一ヶ月くらい前に inf-clojure 導入記事を書いたのに、それ以来何も触れてなかったのでそろそろまた書いてみる。
一ヶ月くらい前に書いたってことはつまりあれから一ヶ月くらいは使っているわけですが、間に少し Cursive を使っていたのでまるまるじゃないですが、だいたい仕事中は inf-clojure 使っています。まぁ不満点はほぼほぼないです。
さくっとコードの全貌を置いておきます。
(add-to-list 'auto-mode-alist '("\\.boot$" . clojure-mode)) (add-to-list 'auto-mode-alist '("\\.cljs$'" . clojure-mode)) (add-to-list 'auto-mode-alist '("\\.cljx$'" . clojure-mode)) (add-to-list 'auto-mode-alist '("\\.cljc$'" . clojure-mode)) (use-package clojure-mode :defer t :config (progn (use-package smart-newline-mode) (use-package clojure-mode-extra-font-locking) (use-package align-cljlet :init (bind-keys :map clojure-mode-map ("C-c j a l" . align-cljlet))) (use-package midje-mode) (use-package clj-refactor :config (cljr-add-keybindings-with-prefix "C-c j")) (use-package clojure-snippets) (use-package inf-clojure :config (progn (setq inf-clojure-prompt-read-only nil) (setq inf-clojure-program "boot repl") (defun my/inf-clojure-refresh () (interactive) (inf-clojure-eval-string "(require '[clojure.tools.namespace.repl]) (apply clojure.tools.namespace.repl/set-refresh-dirs (get-env :directories)) (clojure.tools.namespace.repl/refresh)")) (defun my/find-tag-without-ns (tag) (interactive (list (my/helm-gtags--read-tagname 'tag-without-ns))) (helm-gtags--common '(helm-source-gtags-tags) tag)) (defun my/run-clojure () (interactive) (setq current-prefix-arg '(4)) (call-interactively 'run-clojure) (paredit-mode)) (defun my/in-ns-boot-home () (interactive) (inf-clojure-eval-string "(in-ns 'boot.user)")) (bind-keys :map inf-clojure-minor-mode-map ("C-c C-x" . my/inf-clojure-refresh) ("C-c z" . my/run-clojure) ("C-c C-z" . inf-clojure-switch-to-repl) ("C-c C-h" . my/in-ns-boot-home) ("M-." . my/find-tag-without-ns)))) (defun my/clojure-mode-hook () ;; (add-hook 'before-save-hook 'my/cleanup-buffer nil t) (clj-refactor-mode 1) (inf-clojure-minor-mode 1) (paredit-mode 1) (rainbow-delimiters-mode 1) (smart-newline-mode 1) (helm-gtags-mode 1)) (add-hook 'clojure-mode-hook 'my/clojure-mode-hook)))
あと僕の設定ファイル晒しておくので適当に参照して参考にしてもらえればと。
ayato_p / dotemacs-for-clojure — Bitbucket
解説
inf-clojure の REPL がデフォルトで read-only なのでそれを消しています。
inf-clojure の REPL 起動コマンドを普段使う boot へと設定
REPL の reload をしています。 Cider ならデフォルトでありますが、 inf-clojure にはないのでこんな感じのコードを入れてあげると Cider 同様にツーストロークで reload 出来る。
ちなみにこれは for Boot なので Leiningen だとまた書き方が違うけど、だいたい似たようなコマンドはあった方が便利。
- (defun my/find-tag-without-ns (tag)
helm-gtags の helm-gtags-find-tag を微修正した自分用 find-tag 。 Clojure のネームスペースに対して require :as した場合に 例えば (clojure.string :as str) とかした場合 str/replace とか書くんだけど、デフォルトの helm-gtags-find-tag を使うとカーソル位置の名前をそのまま引っ張ろうとして str/replace がデフォルト値になるのであまり便利じゃない。だから str/ の部分を削除出来るように自分用書いた。ちなみにこれのコードの途中にある (my/helm-gtags--read-tagname 'tag-without-ns) はこんな風に別のとこに定義してある。
(add-to-list 'helm-gtags--prompt-alist '(tag-without-ns . "Find Definition: ")) (defun my/helm-gtags--read-tagname (type &optional default-tagname) (let ((tagname (helm-gtags--token-at-point type)) (prompt (assoc-default type helm-gtags--prompt-alist)) (comp-func (assoc-default type helm-gtags-comp-func-alist))) (if (and tagname helm-gtags-use-input-at-cursor) tagname (when (and (not tagname) default-tagname) (setq tagname default-tagname)) (when (eq type 'tag-without-ns) (setq tagname (first (last (split-string tagname "/"))))) (when tagname (setq prompt (format "%s(default \"%s\") " prompt tagname))) (let ((completion-ignore-case helm-gtags-ignore-case) (completing-read-function 'completing-read-default)) (completing-read prompt comp-func nil nil nil 'helm-gtags--completing-history tagname)))))
- (defun my/run-clojure ()
これは boot repl で起動したくないとき(例えば boot repl -c とか)したいときの為のコマンド。開発中は Boot の REPL サーバー立ち上げてやっていることが多いのでこれが必要。
- (defun my/in-ns-boot-home ()
これは REPL のネームスペースが変更されているときに boot のデフォルトネームスペースに戻すために使う。理由としては boot のデフォルトネームスペースだと require しているコマンドとかが使えるので、結構頻繁にこれは使っている。
あとはだいたいおまけ。 auto-complate も他の場所で定義していますが、たぶん ac-cider がそのまま入っているので、ソースはそれと gtags とかが主に使われていますね。
(setq-default ac-sources '(ac-source-yasnippet ac-source-abbrev ac-source-dictionary ac-source-words-in-same-mode-buffers ac-source-gtags))
clj-refactor も入っていますが nREPL は使っていないので、基本的には require とか import 書くときの便利なとこだけ利用しているというところです。
inf-clojure でも Cider 同様に出来ること
- コード補完
- 定義ジャンプ
- REPL の利用
gtags を利用すればコードの補完や定義ジャンプは基本的にそんなに困らない。例えば Boot や Leiningen でライブラリをチェックアウトするタイミングで、それらのライブラリを解析して gtags を同様に作成出来て、それも定義タグのソースとして利用出来るようにすれば cider と同様の力を手に入れることが出来るとは思うんだけど、まぁコストが高いのでそこまでするつもりはない(せめて Clojure のコアライブラリくらいはやろうかな?という気持ちはあるけど、 inf-clojure でそもそもソースとかドキュメントは引っ張れるのであまり気にしていない)。
inf-clojure では Cider に及ばない部分
個人的にはライブラリの中にまで飛びたいという気持ちは薄いのと補完は gtags レベルでもだいたい十分満足するレベルで使えるのでいいやという気持ち。あとリファクタリング機能はなんだかんだでそんなにいらないという気持ちが強くて理由は幾つかあるけど、リネームくらいなら helm-swoop で代用が効くしなーというのもある(老害脳かも…)。
Cider でも出来ないこと
- Java ライブラリのソースをダウンロードして定義ジャンプ
- デバッガのステップイン
- map 形式のコードを綺麗にフォーマットする
Cursive は出来たけど、この辺は完全に IDE に負けているなぁというところだけど、デバッガなくても REPL あるからあんまり困ること少ない。
まとめ
主に nREPL 関係を使っていないことによる弱さがある。けど、ある程度までなら Cider で使っていたような機能は使えるから困っていない。あと macroexpand-all とかが Cider のようにデフォルトでないけど、これも自分で関数書けばいいだけなんで問題ないです(最近は覚えちゃって clojure.walk/macroexpand-all とか REPL で書いて実行したりすることもたまに)。なので、まぁ Cider 使ってるけど使いこなせてないとか、 cider-nrepl 重いし使いたくないとかいう人は inf-clojure 使ってみてもいいんじゃないんでしょうか。まぁ個人的に誰にでも勧めれるものではないと理解していますが*1、一定数いるであろう Cider を不満に思っているけど代替がない…と思っている人向けに書いてみました。まる。
余談
完全にこの話と関係ないけど、 id:syohex さんが inf-clojure ってまんま同じようなコンセプトのものを昔書いてて笑った。d.hatena.ne.jp
*1:Cider 便利だし
第 3 回かわいい Kotlin 勉強会で LT してきたよ #jkug
だいたい Kotlin の話してきました(嘘です、ごめんなさい)。
Twitter の TL 見ていると「殴り込み」とか「天下一 JVM 武闘会」とか言われてました。まじすまんかった。togetter.com
話した内容とか
タイトルはハッカーと画家の中で書かれている言葉をちょっと借りて最終的には Clojure から Kotlin を使うっていう話をしただけです。元々一ヶ月前にこのスライドも作り終わってて準備万端だったんだけど、話を貰ったタイミングでは何を話そうか迷っててスライドの中にある通り以下のようなものが候補に上がってた。
- Clojure で Kotlin をパースする
これは昔 Emacs 用の kotlin-mode を作ったときに、そもそも正規表現でパースするのがしんどすぎたっていうだけなんですけど、 Kotlin って元々 BNF で言語の文法を定義していてそれを公開しているんですよね。
Grammar
なので、これを元にパースしたら一番正確だし楽だよね、って思ったのとちょうど Clojure に instaparse っていうライブラリがあって、これは BNF 情報を元にパーサーを定義出来てパース出来るという代物なんだけど、いい感じに出来そうじゃね?って思ったのでやろうと思った。
Engelberg/instaparse · GitHub
けど、思いの外、 BNF の定義が単純コピペで使えなくて例えばこんなん
RegularStringPart (used by stringTemplateElement) : <any character other than backslash, quote, $ or newline> ShortTemplateEntryStart: : "$" EscapeSequence: : UnicodeEscapeSequence | RegularEscapeSequence UnicodeEscapeSequence: : "\u" HexDigit{4} RegularEscapeSequence: : "\" <any character other than newline>
< any character other than newline > とかがちょいちょいあるので一気に単純置換もできなくて、めんどくさいなーというので途中でやめちゃいました。ちなみに単純にパーサーだけなら昨日書いたけど pygments が対応しています。なので gtags でシンボルタグなら生成出来るのが今ですね。あいにく ctags はなさそうなので、誰か書いてくれると Emacs/Vim ユーザーが歓喜します。
- Kotlin の Lint ツールを作る
これは上記のが出来ればそもそも「パース出来るか」というところで出来なければ構文に不正があると見なせるので作れるんじゃないかなーと妄想しましたが、流石に時間とモチベーションの都合でやりませんでした。
- Kotlin と Clojure の比較
やっても良かったんですけど Clojure ばっかり書いてて Kotlin ワカンネって思ったのでボツ企画です。
- Clojure 入門
流石にたろうさんに怒られると思ったのでやりませんでした。
- Emacs 入門
同上。
- Kotlin-mode その後
これ、昨年の発表後からどうなったっていう話ですね。簡単に書いておくと、リポジトリごと消しました。理由として Kotlin を書いてないし、 JetBrains が作った言語だけあって IDE 使えっていう無言の圧力凄くて CLI 周りが全然発達していないので、もう Kotlin 書くなら IDEA 使うのがベターウェイじゃない?って思ったからです。まぁ自分が書いてない言語の mode なんて保守出来ないというのもあるんですけどね。
さて、まぁそんなこんなで今回話したように「 Clojure から Kotlin を使う」という話をしようと思ったわけですね。
今回話をした内容は「 Clojure から Kotlin を使おうっていう発想がやばい」とか言われていたんですが、僕が昔読んだ本で当時それなりに好評価を得ていた「 ThoughtWorks アンソロジー」という本がありそれの影響を大きく受けています。

ThoughtWorksアンソロジー ―アジャイルとオブジェクト指向によるソフトウェアイノベーション
- 作者: ThoughtWorks Inc.,株式会社オージス総研オブジェクトの広場編集部
- 出版社/メーカー: オライリージャパン
- 発売日: 2008/12/27
- メディア: 単行本(ソフトカバー)
- 購入: 14人 クリック: 323回
- この商品を含むブログ (81件) を見る
さてこの本の中で第 4 章に「多言語プログラミング」という章があります。その中ではこう書かれています。
解決策を 1 つの言語に詰め込もうとしていた日々は、消え去ろうとしています。私たちは、 Java と CLR という優れたマネージド・ランタイムを持っているので、よりよいツールを使ってそれらのプラットフォームを利用するべきです。多言語プログラミングは、重要な仕事をする既存のすべてのコードを捨てることなしに、解決策をミックスし適用することを可能にしてくれます。これらの 2 つの実績あるプラットフォームで、爆発的に新しい言語が開発されています。開発者として、仕事により適したツールを使って、よりよいコードを書くことができるように、この成長を活用する方法を学ぶべきです。
多言語プログラミング/ThoughtWorks アンソロジー
まぁ僕の発表の仕方が悪かったのとスライドもだいぶ Clojure で全力で殴りに行っている感が否めないのであれなんですけど、いろんな言語がひとつのプロジェクトの中にあってそれぞれが相互にコミュニケーション出来るならそれはそれでいいと思うんですよ。だから僕にとってあれは普通だと思うので何もヤバくないです。ふつーです*1。例えば Kotlin を使えば型安全や null 安全という武器を簡単に手に入れることが出来て、 Clojure を使えばイミュータブルな世界を利用した並行/並列に有利なプログラムが書けると思うのでそういう道もあるのかなーと思ったりしながら今回書いたりしました。ついでに言うなら Clojure も Typed Clojure ってあって静的な型付けを手に入れることは出来るんですが…。
感想
同じ発表者である むろほしさん*2と少しお話していたんですが、完全に Kotlin が Android の人のための言語になってしまったんだなーという感じがあってちょっとさみしいなという気持ちがしました。たぶん 2 年くらい前僕が初めて書いたときくらいは僕みたいな「言語スキー」が好奇の目で見ているか、たろうさんみたいな熱狂的なファンが触っているかくらいだった気がしますが、ここ半年くらいからこっちはずっと Android 一色だなーと。なので、たぶん今回の発表も前回の勉強会であれば受け入れられたんだと思いますが、 Android 使いのための勉強会みたいな感じになってて完全にお呼びでない感あったなと*3。
あと勉強会自体は悪くなかったんですが、発表者が変わる間の時間っていうのがタイムテーブル上存在してなかったので、ちょっとドキドキしましたね。発表者によって多少準備に時間がかかるとかあるかもしれないですし、その準備も与えられた時間内でやれっていうんだったらあれですけど、それだと話す時間が短くなるのでちょっと考えて欲しかったなーくらいです*4。夜だというのと懇親会までやるなら詰めるのが吉だったんでしょうけどね。
C-u 大事 - Emacs のキーバインドが枯渇気味の人へ -
だいたい Emacs 使ってて「だいたい使うなー」という拡張が決まってきた。それは良いんだけど、ちょこちょこ拡張を足そうとするときに使い勝手の良いキーバインドはだいたい埋まってきた気がする。
元々 Emacs それ自身が取ってしまっているのもあれば、メジャーモードとか Cider みたいな超多機能拡張がありとあらゆるキーバインドを持って行こうとする。それで何一つ重複させないのは至難の業だと思う。俗に言う Emacs のキーバインド枯渇 issue である。
話は少し変わるけど、最近 Emacs 拡張のコードをたまに読んだりすることがある。そうしているとあるコード辺がよく出てくることに気がついた。 (interactive "p") というコードである。
調べてみるとこのコード辺、前置引数を受け取ることが出来るということらしいけど、最初何の役に立つのか全く検討がつかなかった。更に調べると以下の記事を見つけた。
お陰でようやくこのキーバインド枯渇問題と向き合うことが出来た。とどのつまり簡単に書くなら C-u と押した回数分だけ 4 の倍数が前置引数として渡るので、コマンドの中でそれを元に実行するコマンドを分けてしまえばいいということのよう。例えば C-u C-x C-f みたいな入力をすると C-x C-f にバインドされているコマンドに対して 4 が渡る。
上記の記事に触発されて最近、僕もこれを多用していて使いやすいなーと感じる使い方が出来るようになってきたのでシェアしておこうと思った。
僕の場合は例えばこういう設定をしていたりする。
(bind-keys :map global-map ("C-x C-r" . helm-projectile-recentf) ("C-x b" . helm-projectile-switch-to-buffer) ("C-x C-f" . (lambda (arg) (interactive "p") (case arg (4 (helm-projectile-find-file)) (t (call-interactively 'find-file))))))
これは projectile を helm インターフェイスから使える helm-projectile という拡張を設定しているのだけど、 find-file はなんだかんだ使いやすいので変えたくないけど、ある条件化のときはプロジェクト配下にあるファイルを全て helm で検索したいので helm-projectile-find-file を使いたいというようなときに、こういう風に切り分けている。
もうひとつ紹介してみる。
(bind-keys :map ac-mode-map ("C-i" . (lambda (arg) (interactive "p") (case arg (4 (ac-complete-with-helm)) (t (auto-complete))))))
これもさっきと同じでデフォルトを素の auto-complete にしておいて、 helm インターフェイスを C-u を一度押した方に設定している。
と、まぁこんな感じで最近色々とキーバインドを増やせたこともあり、 Emacs から更に抜け出せなくなっていっている次第です。
余談
確かこれを簡単に実現出来る拡張を何処かで見たことがある気がするけど、忘れました。ただ、結局コードの量がそんなに増えたり減ったりするわけじゃなさそうだったので、僕はこれでいいやと思ってそのままにしています。