前回、VimからEmacsに乗り換えた記事を書いてから、謎の病(通称:風邪)で床に伏せていました。神の怒りでも買ったのでしょうか。
Lispの神様「なんてひどい.emacs.elを晒しやがる。自称Lisperとして恥を知れ!」
ふと.emacs.elを見返すと、コピペしたコードがひどいことになっていました。冗長だったり似たコードが散在していたり。
そうして僕は1週間も横になりながら.emacs.elをリファクタリングするハメになったのです。はい。まあおかげで.emacs.elがすっきりして統一感が出てきました。
今回はLispの神様へのお詫びとして、僕のリファクタリングの結果、追加されたマクロ5つを紹介したいと思います。
1. add-hookを簡潔に
add-hookは、モードを読み込んだり関数を実行したときに実行する処理を設定する関数です。以下の例ではphp-modeを読み込んだときに実行するコードを無名関数で指定しています。
;; 普通の設定例
(add-hook 'php-mode-hook
#'(lambda ()
(require 'symfony)
(setq tab-width 2)))
たぶんほとんどのEmacs使いの人はこのadd-hookを既に常用しているでしょう。僕の.emacs.elも15個のadd-hookがありました。
ただ、僕にはこのadd-hookが冗長に見えます。Lisperの人に面と向かって言うと怒られそうですが、僕はlambdaが嫌いです。長過ぎです。この場合以下のように書けたほうが見やすいです。
;; 改善案
(add-hook-fn 'php-mode-hook
(require 'symfony)
(setq tab-width 2))
これなら行数が1つ減りますし、ネストも1段浅くなってよい感じです。add-hook-fnの定義は以下。
;; マクロ定義 (defmacro add-hook-fn (name &rest body) `(add-hook ,name #'(lambda () ,@body)))
Emacs Lispは&bodyがないんですね。
2. global-set-keyも簡潔に
.emacs.elを眺めていると、上と同じ理由でlambdaを無駄に多用する表現でglobal-set-keyが目につきました。既に定義されている関数を使う分にはいいのですが、ちょっとした処理を割り当てたいときはlambdaを使わないといけません。
;; 普通の設定例 (global-set-key-fn (kbd "C-M-h") (lambda () (interactive) (move-to-window-line 0))) (global-set-key-fn (kbd "C-M-m") (lambda () (interactive) (move-to-window-line nil))) (global-set-key-fn (kbd "C-M-l") (lambda () (interactive) (move-to-window-line -1)))
僕は一度名前付き関数を定義するほどお行儀がよくないのでこんなことになってるのかもしれませんが…。改善して以下のようにしちゃいました。
;; 改善案 (global-set-key-fn (kbd "C-M-h") nil (interactive) (move-to-window-line 0)) (global-set-key-fn (kbd "C-M-m") nil (interactive) (move-to-window-line nil)) (global-set-key-fn (kbd "C-M-l") nil (interactive) (move-to-window-line -1))
処理によっては引数が必要なので、第2引数が引数リストになっています。上の例では空リストなのでnilです。
;; マクロ定義 (defmacro global-set-key-fn (key args &rest body) `(global-set-key ,key (lambda ,args ,@body)))
add-hook-fnとほとんど同じです。
3. 複数の要素をリストに追加
Emacs Lispには標準でリストに要素を追加する関数add-to-listがあります。
(add-to-list 'load-path "~/.emacs.d/elisp")
ただ、これは1つの要素しか追加することができません。もし複数のリストを追加したければappendしてsetqします。
;; 普通の設定例
(setq exec-path
(append
'("/usr/bin" "/bin"
"/usr/sbin" "/sbin" "/usr/local/bin"
"/usr/X11/bin")
exec-path))
これも冗長な上、よく使う表現なのでappend-to-listというマクロを定義しました。
;; 改善案
(append-to-list exec-path
'("/usr/bin" "/bin"
"/usr/sbin" "/sbin" "/usr/local/bin"
"/usr/X11/bin"))
add-to-listと違って第1引数のクォートがいらないことに注意。第2引数は追加する要素のリストです。それ以外はadd-to-listのように使うことができます。
;; マクロ定義 (defmacro append-to-list (to lst) `(setq ,to (append ,lst ,to)))
4. ライブラリがあるときだけrequireする
MacBookで.emacs.elを書き終えたあと、Ubuntu機のEmacsもセットアップしようと思ってとりあえず同じ.emacs.elを読み込みました。
が、当然のようにエラー。そもそもライブラリとか全然入れてないのでrequireするだけでエラーです。可搬性のかけらもありません。完全にMacBook依存。何年も連れ添ったLinuxのことはもう忘れてしまうのね…。
というわけにもいかないので、とりあえずライブラリがあるかどうか確認してrequireするコードに直しました。
;; 普通の設定例 (when (locate-library "elscreen") (require 'elscreen) ...)
そうすると予想通り.emacs.elがlocate-libraryだらけになりました。マクロマクロ。
;; 改善案 (req elscreen ...)
requireのように第2引数以降は指定できないですが、とりあえず困っていないので。
;; マクロ定義
(defmacro req (lib &rest body)
`(when (locate-library ,(symbol-name lib))
(require ',lib) ,@body))
require-when-locateとか、require-if-existsとかいろいろ名前を考えてみましたが、長すぎるのも嫌なのでいっそreqにしました。
これはdefadviceで書けって言われそうですが、adviceの書き方を把握していないのでdefmacroで :p
5. autoloadで遅延ロード
Emacs Lispでは、外部ファイルの読み込み方法がいくつもあってよくわかりません。基本的にはrequireしてたら使えるようですが、必要ないファイルまで毎回読み込むのはクールじゃありません。普通は毎回使うファイルは違います。Ruby書きたいときにphp-modeは必要ないです。
必要なファイルだけを読み込む方法として、autoloadがあります。これは、ファイルのロードを特定の関数が呼び出されたときまで遅延します。英語で言うとLazyです。Lazy大好きです。
;; 普通の設定例1 (autoload 'php-mode "php-mode" nil t)
ただ、この場合もrequireのようにlocate-libraryを確認してからautoloadするようにしたほうがいいです。
;; 普通の設定例2 (when (locate-library "php-mode") (autoload 'php-mode "php-mode" nil t))
もしこのライブラリが読み込まれたあとに評価したい処理があるときはeval-after-loadを使います。
;; 普通の設定例3
(when (locate-library "php-mode")
(autoload 'php-mode "php-mode" nil t)
(eval-after-load "php-mode"
'(progn
(req symfony)
(setq tab-width 2))))
おっと。なんて冗長なコードだろう。ライブラリ名が3回も出ている。これもreqみたいにマクロ化します。
;; 改善案
(lazyload (php-mode) "php-mode"
(req symfony)
(setq tab-width 2))
第1引数はフックする関数のリストです。リストなので、複数の関数をフックできます。第2引数はライブラリ名、あとはeval-after-loadする処理です。
;; マクロ定義
(defmacro lazyload (func lib &rest body)
`(when (locate-library ,lib)
,@(mapcar (lambda (f) `(autoload ',f ,lib nil t)) func)
(eval-after-load ,lib
'(progn
,@body))))
このとき、処理本体の部分にadd-hookを入れてはいけません。本体はライブラリを読み込んでからしか評価されないのに、フックされないと読み込まれない、という鶏と卵状態になってしまいます。
;; 使用例
(lazyload (php-mode) "php-mode"
(req symfony))
;; add-hookは外に書く
(add-hook-fn 'php-mode-hook
(setq tab-width 2)
(c-set-offset 'arglist-intro '+)
(c-set-offset 'arglist-close 0))
まとめ
以上、自称Lisperがリファクタリングしてみました。
僕の.emacs.elの完全なコードはgistに上がっています。興味があれば今回紹介したマクロの使用例として参考にしてください。
他の人はどういう最適化をしてるんでしょう? こういう情報は少ない気がするのでどんどん共有していきたいです
※追記 : コメント欄でsakitoさんからEmacs Lispのマクロ記事を紹介してもらいました。似たアプローチなので下の記事も参考にしてください。




この手の物の一部は
http://www.sodan.org/~knagano/emacs/dotemacs.html
が参考にされている率が高いかとおもいます。
おぉ、まさにこういう情報が欲しかったです!
ありがとうございます。追記しておきます :)