custom-set-variables は使わない方が良いかもしれない

年末大掃除ということで Emacs の設定を見直しています。以下、Emacs 24.3.1 の内容です。

Emacs には defcustom というマクロが提供されており、パッケージ開発者はユーザが変更できる変数を定義する際に defvar でなくこちらを使うのが一般的です。
defcustom で定義された変数の場合、ユーザは customize-variable 関数など customize-* 関数で手軽に値を変更することができます。
customize-* 関数で変更した変数は custom-set-variables 関数によって変数名(シンボル)と値のペアが ‘user の ‘theme-settings 1 に登録されるよう、~/.emacs.d/init.el に次のような内容が挿入されます。

(custom-set-variables
 ;; custom-set-variables was added by Custom.
 ;; If you edit it by hand, you could mess it up, so be careful.
 ;; Your init file should contain only one such instance.
 ;; If there is more than one, they won't work right.
 '(inhibit-startup-screen t))

この custom-set-variables 関数ですが、値をセットする際にカスタム変数に定義されたセッター関数を実行するなど setq 関数とは違った機能があるため、こちらを使った方が良さそうな気がします。

っで、ちょっと調べてみたところ、個人的には何も考えずパッケージをロードする前に setq 関数で値をセットするのが良いのではないかと思います。
理由は以下の 3 つ。

  1. defcustom マクロを実行する前に custom-set-variables 関数を実行すると意図しない結果になる場合があるから
  2. カスタム変数に initialize キーワードが設定されていると値が無視される可能性があるから
  3. カスタム変数にセッターを定義する開発者がそれほど多くない気がするから

結局のところ、setq 関数で値をセットして問題があるのはそのカスタム変数に依存した変数が更新されないことにあります。なので、ファイルをロードする前にカスタム変数の値をセットしておけば問題ないはずです。

解説

上記の例に対して少し解説します。

defcustom マクロを実行する前に custom-set-variables 関数を実行すると意図しない結果になる場合がある

custom-set-variables では ‘user の ‘theme-settings に変数を登録する際に、変数の依存関係を考慮してソートします。これがおそらく custom-set-variables を複数箇所に記述するなと言われる理由だと思います。
変数の依存関係は defcustom マクロで set-after キーワードを指定することで記述可能です。
ところが、ファイルをロードしない限りこの set-after キーワードの存在を確認できないため、変数の依存関係を考慮してソートできません。2

例えば次のような foo というパッケージを作成したとします。

;;; foo.el
(defun foo-set-value-with-message (variable value)
  (message "set %s to %s" variable value)
  (set-default variable value))

(defcustom foo-custom-var1 "foo-custom-var1"
  "Document of `foo-custom-var1'"
  :set 'foo-set-value-with-message
  :require 'bar
  :set-after '(foo-custom-var2))

(defcustom foo-custom-var2 "foo-custom-var2"
  "Document of `foo-custom-var2'"
  :set 'foo-set-value-with-message)

(provide 'foo)

foo-custom-var1 には set-after キーワードとして ‘(foo-custom-var2) を指定しているで、値をセットする前に foo-custom-var2 の値がセットされることを期待しています。
これに対し、次のような内容を初期化ファイルに記述すると、set-after は無視されます。

(custom-set-variables
 '(foo-custom-var1 "my-foo-custom-var1")
 '(foo-custom-var2 "my-foo-custom-var2"))
(require 'foo)  ; => set foo-custom-var1 to my-foo-custom-var1
                ;       set foo-custom-var2 to my-foo-custom-var2

どういった場合に弊害があるかというと、foo-custom-var1 のセッター関数内で foo-custom-var1 と foo-custom-var2 両方に依存した変数を更新するロジックが書かれていた場合、foo-custom-var2 が古いままということになります。3

なので、先に require しておく必要があります。

(require 'foo)  ; => set foo-custom-var1 to my-foo-custom-var1
                ;       set foo-custom-var2 to my-foo-custom-var2
(custom-set-variables
 '(foo-custom-var1 "my-foo-custom-var1")
 '(foo-custom-var2 "my-foo-custom-var2"))  ; => set foo-custom-var1 to my-foo-custom-var2
                                           ;       set foo-custom-var1 to my-foo-custom-var1

autoload などを設定している場合は関数が呼ばれた時に初めてロードされるので、初期化ファイルで custom-set-variables を実行されると確実に set-after が無視されることになります。

カスタム変数に initialize キーワードが設定されていると値が無視される可能性がある

initialize キーワードが設定されていない場合、custom-set-variables 関数で登録した変数は defcustom マクロ実行時に次の関数で初期化されます。

(defun custom-initialize-reset (symbol value)
  "Initialize SYMBOL based on VALUE.
Set the symbol, using its `:set' function (or `set-default' if it has none).
The value is either the symbol's current value
 \(as obtained using the `:get' function), if any,
or the value in the symbol's `saved-value' property if any,
or (last of all) VALUE."
  (funcall (or (get symbol 'custom-set) 'set-default)
           symbol
           (cond ((default-boundp symbol)
                  (funcall (or (get symbol 'custom-get) 'default-value)
                           symbol))
                 ((get symbol 'saved-value)
                  (eval (car (get symbol 'saved-value))))
                 (t
                  (eval value)))))

custom-set-variables 関数は変数を表すシンボルに ‘saved-value をセットすることで遅延評価をしているわけですが、defcustom マクロ実行時に (eval (car (get symbol ‘saved-value))) によって値を取り出していることがわかります。
つまり、initialize キーワードが設定されている場合はこのロジックが入っていない限り custom-set-variables 関数で指定した値は無視されます。
ただし、既に変数が定義されている場合は指定した値で即上書きするのでそんなことはありません。

カスタム変数にセッターを定義する開発者がそれほど多くない気がする

custom-set-variables 関数でカスタム変数をセットすることの最大の魅力は、開発者がセッター関数を定義しておけばそのカスタム変数に依存した変数も更新することができることです。
が、そこまで考慮してパッケージ開発をしている開発者はそこまで多くない気がします。その場合、setq 関数で値をセットする場合と変わらないことになります。

前の 2 つの例から custom-set-variables 関数を実行するのはパッケージのロード後の方が良さそうですが、セッター関数が定義されていないのであればロード前に実行する方が良さそうです。

以上から、個人的にはパッケージをロードする前に setq 関数を使って全ての変数をセットするのが良いと思います。メジャーモードや用途ごとに変数の定義箇所を心置きなく分けられますし。パッケージがロードされてなくても変数が定義されてしまうのは少し嫌ですが。

1 点補足しておくと、

defcustomには:set以外にも,変数に値が設定された際に特定のパッケージやファイルを読み込む:requireや:loadというオプションもあるので,パッケージ作者が意図したとおりの挙動を保証するためにも,defcustomで定義された変数の変更には常にcustom-set-variablesを用いるべきである.

引用:defcustomで定義された変数はsetqではなくcustom-set-variablesで設定すべき理由 - kawamuray’s blog

setq 関数を使って事前に値をセットしておいた場合、custom-initialize-reset 関数で分岐が異なるだけなので、この点については現行バージョンでは心配しなくて良さそうです。

  1. cf. (get ‘user ‘theme-settings) 

  2. カスタム変数に autoload cookie を設定してもダメみたいです 

  3. どちらのセッター関数にも更新ロジックを記述しておいて 2 回実行すればいいんですが