defcustom で初期化処理を行う

midnight.el がなぜ require しただけで動作するのかを検証したとき、 defcustom に初期化処理ができることがわかった。
新しい Emacs を立ち上げて midnight.el を読み込む。たとえば「 emacs -Q -l midnight 」を実行する。すると、 defvar で nil と宣言されているはずの midnight-timer が timer object になっているではないか。いったいなぜ?
常用している anything-c-moccur で midnight-timer を探してみると、 midnight-delay-set 関数にて setq されているのがわかる。で、その関数はどこから呼ばれているかというと、ここ。

(defcustom midnight-delay 3600
  "*略"
  :type 'sexp
  :set 'midnight-delay-set
  :group 'midnight)

マニュアルを読んでみると、 set で定義されている関数を使って変数をセットしていることがわかった。ふむふむ、変数名と値を引数とした関数を指定できるのな。それが midnight-delay-set 関数。そこでは「 (set sym tm) 」で値をセットして、ついでに midnight-timer もセットしているじゃないか。

では、最小限のサンプルを作成して検証してみる。

;; (save-window-excursion (shell-command (format "emacs -no-site-file -q -L ~/emacs/lisp -l %s %s " buffer-file-name buffer-file-name)))
(defgroup test nil
  "test group")
(setq set-test-called nil)
(defun set-test (symb v)
  (setq set-test-called t)
  (set symb v))
(defcustom testv 12
  "*doc"
  :type 'sexp  
  :group 'test
  :set 'set-test)
(message "set-test-called: %s" set-test-called)

emacs -Q -l defcustom-test.el を実行すると、「 set-test-called: t 」と表示される。さらに、何度 load しても同じ結果になる。 defcustom を評価した時点で実行されるようだ。
もちろん、「(setq testv 7)」みたいに直接 setq すると set-test は実行されない。だからこそ、こういう変数はロード前に setq しておく必要があるんだな。納得。