読者です 読者をやめる 読者になる 読者になる

defcustomで定義された変数はsetqではなくcustom-set-variablesで設定すべき理由

Emacs elisp

Emacsにおけるこの辺の事情ってほんとうにややこしくて,ドキュメントでもパッケージによってsetq使ってたり,setq-default使ってたり,custom-set-variables使ってたりで統一されていないので混乱のもとになっている気がする.

"defcustom setq custom-set-variables"とかでググると,日本語でこのあたりを話題にしているのはこのエントリくらいなのだけど,結論としては明らかにcustom-set-variablesを使うべきである.

(defcustom foo-variable nil
  "Foo variable")

というような定義があった場合,foo-variableはnilで初期化される.ただし,既に値が設定されていない場合に限る.つまり,上のdefcustomがbarというパッケージの中に書かれたものだとして,

(require 'foo)
(setq foo-variable "hoge")
(setq foo-variable "hoge")
(require 'foo)

という順序を逆にした二つの設定をかいた場合,いずれも実行後にfoo-variableには"hoge"が設定されているが,パッケージによっては,後者しか正しく動作しないことがある.これは,パッケージが読み込まれた時点でfoo-variableを使った処理(define-keyとか)が行われるような場合,パッケージが読み込まれた時点での変数の値が作用するからである.

では,

(require 'foo)
(custom-set-variables '(foo-variable "hoge"))
(custom-set-variables '(foo-variable "hoge"))
(require 'foo)

の二つではどうかというと,こちらは(パッケージ作者がvariable customization systemをきちんと意識した実装をしていれば)どちらも等価であると言える.

これは,custom-set-variablesの機能が単純に,当該シンボルに値をsetするだけではないためである.defcustomのマニュアルを読めば分かるが,defcustomには細かいオプションが用意されていて,次のような定義が可能である.

(defcustom foo-variable nil
  "Foo variable"
  :set (lambda (var val)
        (set var val)
        (update-foo)))

:setで指定されているのは,foo-variableというカスタム変数に対するセッタである.:setを指定しない場合,set-defaultという組み込み関数が使用されるが,値が更新された時に内部状態をアップデートしたりする必要があるパッケージでは,この仕組みを用いることで,パッケージの読み込み/初期化が終わった後でカスタム変数が再設定されても,その変更を適切に反映できるようになっている.

setqやsetq-defaultを使用してこれらの変数を変更してしまった場合,当然指定したセッタは使用されず,変数に値がbindされるだけであるため,パッケージが正しく動作する保証はない.

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

# defcustomには:typeというオプションもあって,これは値が設定される際に値の型をチェックするための指定だと思っていたのだが(というか個人的にはそうするべきだと思うのだが),実はM-x customize-set-variable した時のプロンプト用に使われているだけっぽかった.