Emacs
EmacsDay 7

use-packageで可読性の高いinit.elを書く

More than 1 year has passed since last update.

Emacsの設定は ~/.emacs.d/init.el に記述しますが、複数のマシンを利用することが当然な昨今、 init.el を複数環境で共有するのはよくあるシチュエーションです。そこで問題になるのは、動作するマシンによってOSやインストールしている外部ライブラリが異なることです。多くの init.el では、ライブラリの存在を確認してからロードしたり、安全にライブラリをロードする自作マクロを活用しています。

またEmacsは起動時間を短縮するために、ライブラリの遅延ロードが可能です。しかしその設定が複雑になってくると、可読性は低くなってしまいます。

use-packageinit.el の設定を、統一的なわかりやすい記述で書くことができるようになるライブラリです。ユーザは use-package マクロを利用するだけで、ライブラリのロード、遅延ロード、設定をわかりやすく記述することができます。

この記事では use-package を利用して、可読性が高く、高速に起動し、環境に非依存な init.el を書く方法を紹介します。なるべくわかりやすくなるよう、use-package を使った場合と、使わない場合のコードを併記し、典型的なユースケースなどについても解説します。

use-package の使い方

requireの置き換え

まずはじめは単純な require の置き換えです。置き換えの前後を比較してみます。

Before
(require 'xxx)
use-packageによる記述
(use-package xxx)

Beforeの記述では xxx ライブラリが存在しない場合エラーが発生し、評価がそこで終わってしまいます。 use-package マクロは、 xxx ライブラリの有無を自動でチェックします。ライブラリが存在する場合はそれをロードし、存在しない場合は何もしません(エラーも起こりません)。このためユーザはライブラリの有無を意識することなく init.el を書くことができます。 use-package マクロはライブラリ名にクォートが必要ないことに注意しましょう。

これが use-package マクロの一番シンプルな使い方となります。さらにキーワード引数をつけることで、 use-package マクロに様々な動作を付け加えることができます。

:config - ライブラリの設定を記述

:config キーワードはライブラリをロードした後の設定などを記述します。

Before
(when (require 'uniquify nil t)
  (setq uniquify-buffer-name-style 'post-forward-angle-brackets)
  (setq uniquify-ignore-buffers-re "*[^*]+*"))
use-packageによる記述
(use-package uniquify
  :config
  (setq uniquify-buffer-name-style 'post-forward-angle-brackets)
  (setq uniquify-ignore-buffers-re "*[^*]+*"))

Beforeの (when (require 'uniquify nil t) ...)init.el でよくあるイディオムです。 require は第3引数が t の時、ライブラリが存在しない場合にエラーを発生させる代わりに nil を返します。条件分岐と組み合わせることにより、ライブラリが存在するときだけ後続のライブラリ設定を評価することができます。

use-package の記述では先ほど同様 uniquify の有無は自動でチェックされます。存在する場合はライブラリをロードした後に :config に記述された式1を評価します。当然存在しない場合は何もしません。

:if - 条件分岐

:if キーワードはライブラリをロードする条件を記述します。 条件が nil と評価される場合は use-package マクロは何もしません。

典型的なユースケースはEmacsが動作しているOSによってライブラリをロードするか切り替える場合です。例えばcygwin-mount.elは、WindowsのEmacsでCygwinのパスを解釈できるようにするライブラリです。これを動作OSがWindowsの場合のみロードするには以下のように記述します。

Before
(when (eq system-type 'windows-nt)
  (when (require 'cygwin-mount nil t)
    ;; cygwin-mountの設定
    ))
use-packageによる記述
(use-package cygwin-mount
  :if (eq system-type 'windows-nt)
  :config
  ;; cygwin-mountの設定
  )

Beforeの記述は条件文や入れ子によってやや見難くなっています。 use-package の場合、キーワードによって条件部分や設定部分が明確に分かれており非常に理解しやすい記述となります。

他にも

  • 外部コマンドの有無を executable-find で判定(migemo、magitなど)
  • 端末で動作しているかを window-system で判定

などの外的環境によって条件分岐するユースケースがありそうです。

遅延ロード

require は評価した瞬間にライブラリをロードするため、 init.el で大量にrequire を書くとEmacsの起動時間が長くなってしまいます。Emacsには起動時ではなく、必要とされた時にライブラリをロードする autoload という遅延ロードの仕組みがあります。

その使い方はシンプルで以下のように設定します。

(autoload 'xxx-command "xxx")

この設定により xxx-command という関数を実行しようとした瞬間に、 xxx ライブラリラリがロードされます。

use-package マクロで遅延ロードの設定をするには、 :commands, :bind, :mode, :interpreter, :defer の5つのキーワードを利用します(これら5つをまとめて遅延キーワードと呼ぶことにします)。遅延キーワードがつけられたライブラリはEmacs起動時にはロードされず、必要なタイミングで遅延ロードされることになります。ここからは遅延キーワードと、それに非常に関連深い :init キーワードの具体的な使い方を見てみます。

:commands - autoload するコマンドを指定

例えば winner 2を遅延ロードする設定を見てみましょう。

Before
(autoload 'winner-undo "winner" "" t)
(autoload 'winner-redo "winner" "" t)
(eval-after-load 'winner
  '(progn
     ;; winnerの設定
     ))

winner で利用するコマンドは winner-undowinner-redo があるので、それらを autoload します。また winner の設定は、 winner のロード後に実行されるように eval-after-load を使います。これで M-x winner-undo などでコマンドを実行しようとした時に、 winner が遅延ロードされます。

一方の use-package を使った記述はこうなります。

use-packageによる記述
(use-package winner
  :commands (winner-undo winner-redo)
  :config
  ;; winnerの設定
  )

:commands キーワードには autoload するコマンド群をリストで指定します3。上述した :config キーワードを使えば、 winner をロードした後の設定が記述できます。 autoload を列挙する必要もなく、 eval-after-load の記述もなくなっているので、読みやすくなりました。

:bind - キー割り当てを指定

autoload したコマンドをキー割り当てすれば、ショートカットキーでコマンドを呼び出そうとした瞬間に遅延ロードされます。

例えば multiple-cursors を遅延ロードしてみましょう。

Before
(autoload 'mc/mark-next-like-this "multiple-cursors")
(autoload 'mc/mark-previous-like-this "multiple-cursors")
(autoload 'mc/mark-all-dwim "multiple-cursors")
(global-set-key (kbd "C->") 'mc/mark-next-like-this)
(global-set-key (kbd "C-<") 'mc/mark-previous-like-this)
(global-set-key (kbd "M-R") 'mc/mark-next-all-dwim)
(eval-after-load 'multiple-cursors
  '(progn
     ;; multiple-cursorsの設定
     ))

この設定をして C-> キーを押下すると、以下の様な流れで multiple-cursors が遅延ロードされます。

  • C-> 押下
  • mc/mark-next-like-this 実行、する前に multiple-cursors ライブラリをロード
  • eval-after-load で書かれた設定を評価
  • mc/mark-next-like-this 実行

autoloadglobal-set-key が繰り返されており、いかにも冗長な記述です。この設定を use-package で書くとこうなります。

use-packageによる記述
(use-package multiple-cursors
  :bind (("C->" . mc/mark-next-like-this)
         ("C-<" . mc/mark-previous-like-this)
         ("M-R" . mc/mark-all-dwim))
  :config
  ;; multiple-cursorsの設定
  )

:bind には (割り当てキー . 実行コマンド) のリストを指定します。ここで指定された実行コマンドは、 use-package マクロが自動的に autoload とキー割り当てをしてくれます。

典型的なユースケースはメジャーモードでもマイナーモードでもない、便利コマンドを提供しているようなライブラリです。私の場合 direx, open-junk-file, color-moccur, bm などのライブラリで使っています。

:mode - auto-mode-alist の設定

Emacsには拡張子とメジャーモードの関連付けを設定する auto-mode-alist という変数があります。例えばhtmlやテンプレートを編集するメジャーモードであるweb-modeの設定を見てみましょう。

Before
(autoload 'web-mode "web-mode")
(add-to-list 'auto-mode-alist '("\\.html?\\'" . web-mode))
(add-to-list 'auto-mode-alist '("\\.jsp\\'"   . web-mode))
(add-to-list 'auto-mode-alist '("\\.gsp\\'"   . web-mode))
(eval-after-load 'web-mode
  '(progn
     ;; web-modeの設定
     ))

これでhtmlファイルを開くと以下の様な流れで web-mode が遅延ロードされます。

  • htmlファイルを開く
  • auto-mode-alist から関連付されたメジャーモードが web-mode とわかる
  • web-mode 実行、する前に web-mode ライブラリをロード
  • eval-after-load で書かれた設定を評価
  • web-mode を実行し、メジャーモードが web-mode になる

auto-mode-alist への追加設定が繰り返されており、これまた冗長な記述です。この設定を use-package で書くとこうなります。

use-packageによる記述
(use-package web-mode
  :mode (("\\.html?\\'" . web-mode)
         ("\\.jsp\\'"   . web-mode)
         ("\\.gsp\\'"   . web-mode))
  :config
  ;; web-modeの設定
  )

:mode には (拡張子の正規表現 . メジャーモード) のリストを指定します。典型的なユースケースはずばりメジャーモードです。

:interpreter - interpreter-mode-alist の設定

auto-mode-alist は拡張子とメジャーモードの関連付けでしたが、シバンに書かれたインタープリタとメジャーモードを関連付ける interpreter-mode-alist という変数があります。使い方はほぼ :mode キーワードと同じなので、 ruby-mode の記述例だけ載せておきます。

Before
(autoload 'ruby-mode "ruby-mode")
(dolist (name (list "ruby" "rbx" "jruby" "ruby1.9" "ruby1.8"))
  (add-to-list 'interpreter-mode-alist (cons name 'ruby-mode)))
(eval-after-load 'ruby-mode
  '(progn
     ;; ruby-modeの設定
     ))
use-packageによる記述
(use-package ruby-mode
  :interpreter (("ruby"    . ruby-mode)
                ("rbx"     . ruby-mode)
                ("jruby"   . ruby-mode)
                ("ruby1.9" . ruby-mode)
                ("ruby1.8" . ruby-mode))
  :config
  ;; ruby-modeの設定
  )

:interpreter には (インタープリタ名 . メジャーモード) のリストを指定します。典型的なユースケースは、スクリプト言語のメジャーモードです。

:init - 初期化コードの設定

:init キーワードは :config と同様にライブラリに関する設定を記述するキーワードですが、遅延キーワードと同時に使用されるかによって評価順がかわります。遅延キーワードがない場合は、以下の順で評価されます。

  1. ライブラリのロード
  2. :init キーワードの設定を評価
  3. :config キーワードの設定を評価

すべてのコードはEmacs起動時に評価されます。

一方遅延キーワードと同時に使用した場合は、以下の順になります。

  1. :init キーワードの設定を評価
  2. autoload された関数が実行されるタイミングで)ライブラリの遅延ロード
  3. :config キーワードの設定を評価

Emacs起動時に評価されるのは :init の設定のみです。この性質により :init キーワードを利用すれば、 遅延キーワードでは表現しきれない遅延ロードの設定が可能になります。

その典型的なユースケースが、特定のキーマップに対するキー割り当てです。:bind キーワードは global-map に対するキー割り当てしかできないため、他のキーマップに割り当てをしたい場合は :bind が使えません。そこで :init キーワードを使ってキー割り当てを記述します。

:bind キーワードの例に出した multiple-cursors のコマンドを、 global-map ではなく mode-specific-map (C-c) 以下に割り当てる場合は以下の様な記述になります。

(use-package multiple-cursors
  :commands (mc/mark-next-like-this mc/mark-previous-like-this mc/mark-all-dwim)
  :init
  (bind-keys :map mode-specific-map
             ("C->" . mc/mark-next-like-this)
             ("C-<" . mc/mark-previous-like-this)
             ("M-R" . mc/mark-all-dwim))
  :config
  ;; multiple-cursorsの設定
  )

:commands キーワードを使って必要なコマンドを autoload し、 bind-key 4でキー割り当ての設定をしています。

別のユースケースとしては hook の設定が必要なケースです。例えば highlight-symbolruby-mode のバッファで有効にする場合は以下のように記述します。

(use-package highlight-symbol
  :commands highlight-symbol-mode
  :init
  (add-hook 'ruby-mode-hook 'highlight-symbol-mode)
  :config
  ;; highlight-symbolの設定
  )

この設定によりEmacs起動時には highlight-symbol はロードされず、 ruby のファイルを開いた時(あるいは明示的に ruby-mode を実行した時など)に遅延ロードされます。多くのマイナーモードの設定はこのような形で記述できると思います。

:defer - 遅延ロードの宣言

:defer キーワードはライブラリが遅延ロードされることを宣言するだけで、特に遅延ロードの設定はしません。なぜこのようなキーワードがあるかというと、遅延ロードの設定をするまでもなく、Emacsを起動するだけで遅延ロードの設定が完了しているライブラリがあるからです。

例えばEmacsでInfoを読む info というライブラリがあります。このライブラリはソースコード中に ;;;#autoload というマジックコメントが付いており、必要なコマンドは自動で autoload されます5。また info を呼び出すキーバインドもデフォルトで設定されているため、ユーザ側で use-package マクロの遅延ロード設定する必要がありません。だからといって以下のように記述すると問題があります。

(use-package info
  :config
  ;; infoの設定
  )

この記述ではEmacs起動時に info ライブラリがロードされてしまい、せっかくの autoload の意味が無くなってしまいます。

そこで以下のように :defer キーワードで遅延ロードであること明示すると、起動時にロードされることはなくなり、M-x info を呼び出したり C-h i を押下したタイミングで遅延ロードすることができます。

(use-package info
  :defer t
  :config
  ;; infoの設定
  )

多くの標準ライブラリは autoload マジックコメントが使われており、ユーザが明示的に autoload を書く必要があるケースはあまりありません。どのコマンドをautoload するべきかは当然ライブラリ開発者が一番理解しているので、それに任せるのが妥当です。

また package を使っていれば非標準ライブラリでも同様のことが可能になります。package が自動で autoload マジックコメントを抽出し、 autoload の設定をしてくれます。上述した multiple-cursorshighlight-symbol にもautoload マジックコメントが付けられているため6、実を言うと :defer t をつけるだけでいいのです。

明示的なautoloadは必要ない
(use-package multiple-cursors
  :defer t
  :init
  (bind-keys :map mode-specific-map
             ("C->" . mc/mark-next-like-this)
             ("C-<" . mc/mark-previous-like-this)
             ("M-R" . mc/mark-all-dwim))
  :config
  ;; multiple-cursorsの設定
  )

(use-package highlight-symbol
  :defer t
  :init
  (add-hook 'ruby-mode-hook 'highlight-symbol-mode)
  :config
  ;; highlight-symbolの設定
  )

遅延ロードとライブラリの存在確認

ライブラリが遅延ロードされる場合、ライブラリの存在確認も遅延されます。これは結構困った問題を引き起こします。例えば以下の設定を記述した上で highlight-symbol ライブラリが存在しない場合どうなるでしょうか?

(use-package highlight-symbol
  :defer t
  :init
  (add-hook 'ruby-mode-hook 'highlight-symbol-mode)
  :config
  ;; highlight-symbolの設定
  )

rubyのファイルを開くと、 Symbol's function definition is void: highlight-symbol-mode とエラーが起きてしまいます。これは :init キーワードの設定が highlight-symbol の有無にかかわらず実行されてしまうためです7

これを避けるには遅延ロードの場合でも、Emacs起動時にライブラリの存在を確認する必要があります。実はそのような機能がこのプルリクで実装されたみたいなのですが、起動時間が遅くなってしまうというのが原因でrevertされちゃってます。

ライブラリの存在確認をする locate-library が結構遅いので、すべてのuse-package マクロで locate-library するのは時間がかかってしまうとのこと。プルリクでは最終的に、起動時間を優先するか存在確認を優先するか設定するオプションつくろうぜ、っていう話になってるんですがそのまま音沙汰が無いようです。

無理やりやろうと思えば :if キーワードを使って自分で存在確認をしてしまえばいいです。

(use-package highlight-symbol
  :if (locate-library "highlight-symbol")
  :defer t
  :init
  (add-hook 'ruby-mode-hook 'highlight-symbol-mode)
  :config
  ;; highlight-symbolの設定
  )

ただあんまり美しい方法ではないので、やっぱりオプションがほしいですね。

その他こまごましたこと

use-package が存在しない場合

use-package ライブラリが存在しない場合、当然 use-package マクロが存在しないので init.el の評価が途中で失敗します。 use-package ライブラリが存在しない時は、何もしない use-package マクロを定義しておくと失敗しなくなるので精神衛生上よろしいと思います。

何もしないマクロ定義
(unless (require 'use-package nil t)
  (defmacro use-package (&rest args)))

:disabled - 設定の無効化

use-package マクロに :disabled t キーワードをつけると、そのライブラリの設定はまるまる無視されます。一時的にライブラリを無効化したい場合などに便利です。

まとめ

use-package を使って可読性の高い init.el を書く方法を紹介しました。まとめとして各キーワードの簡単な説明を表にしておきます。

キーワード 説明 遅延 形式
:config 設定コード   ロード後に評価する式(複数でもOK)
:init 初期化コード   ロード前に評価する式(複数でもOK)
:if 場合分け   条件文
:commands autoload command or そのリスト
:bind autoload+キー割り当て (key . command) or そのリスト
:mode autoload+auto-mode-alist設定 regex or (regex . mode) or そのリスト
:interpreter autoload+interpreter-mode-alist設定 regex or (regex . mode) or そのリスト
:defer 遅延ロード boolean
:disabled 無効化   boolean

すべてのキーワードを使うのは大変ですが、既存の設定を use-package マクロと :config キーワードを使って書き換えるのは非常に簡単です。その後は少しずつ遅延キーワードを使っていって、起動時間を短くしていけばいいと思います。

最後に私も2日めの@tadsan同様@kawabataさんのdotfiles/init.el at master · kawabata/dotfilesに大いに影響を受けています。そもそも use-package の存在を知ったのが、8月に開催された関東Emacsでの@kawabataさんのお話でした。use-package の書き方の参考にもさせていただきました。ここに多大なる感謝を申し上げます。

参考URL

Footnotes:

1 複数の文でも構いません。 use-package が勝手に progn で囲んでくれます。
2 winner はウィンドウ構成のアンドゥ、リドゥが可能になるライブラリです。
3 autoload するコマンドが1つの場合はリストでなくても良いです。
4 bind-keyuse-package に同梱されているライブラリで、キー割り当てを簡潔に記述することができます。詳しい使い方はemacs bind-key.el : るびきち「日刊Emacs」でどうぞ。
5 /usr/share/emacs/${version}/lisp/loaddefs.el あたりに autoload の 設定が列挙されています。
6 ~/.emacs.d/elpa/${library-name}/*-autoloads.el というファイルに autoload の設定が列挙されています。
7 例に出したのが :init キーワードなだけで、遅延キーワードでも起こりえます。