Emacs 24.3 で導入された汎変数を使えば、ソースコードの可読性が高まる可能 性がある。
setf の例
汎変数の代入マクロ setf を使えば、 car だけで setcar 相当が記述できる。
(setq a (cons 3 5)) (setf (car a) 10) a → (10 . 5)
setcar/setcdr という関数を別に覚えるよりは、学習コストは低い。同様に、
(setq a (list 1 10 100 1000 10000)) (elt a 3) → 1000 (setf (elt a 3) 300) a → (1 10 100 300 10000)
となる。
setf に対応している関数
24.3 で、setf に対応している関数は以下の通り。setf を使えば右の関数は 不要になる。覚える関数が減れば、プログラム作成・保守も楽になる。
| 汎変数の対象 | 対応する設定関数 |
|---|---|
| aref | aset |
| car | setcar |
| cdr | setcdr |
| caar | setcar/car |
| cadr | setcar/cdr |
| cdar | setcdr/car |
| cddr | setcdr/cdr |
| elt | setcar/nthcdr |
| get | put |
| gethash | puthash |
| nth | nthcdr/setcar |
| nthcdr | nthcdr/setcdr |
| default-value | set-default |
| frame-parameter | set-frame-parameter |
| terminal-parameter | set-terminal-parameter |
| keymap-parent | set-keymap-parent |
| match-data | set-match-data |
| overlay-get | overlay-put |
| overlay-start | move-overlay |
| overlay-end | move-overlay |
| process-buffer | set-process-buffer |
| process-filter | set-process-filter |
| process-sentinel | set-process-sentinel |
| process-get | process-put |
| window-buffer | set-window-buffer |
| window-display-table | set-window-display-table |
| window-dedicated-p | set-window-dedicated-p |
| window-hscroll | set-window-hscroll |
| window-parameter | set-window-parameter |
| window-point | set-window-point |
| window-start | set-window-start |
| progn | |
| let, let* | |
| cond | |
| if | |
| cons | |
| logand |
汎変数を使うと、構造の構築子とアクセス関数のペアだけで、構造に式を代入 できる。たとえば hash の場合、これまではmake-hash-table, gethash の他に puthash が必要だったが、setf を使えばputhash は不要になる。
(puthash key val table) ≅ (setf (gethash key table) val)
汎変数の作り方
gv.el の最後には、コメントアウトされている alist-get 関数の実装例があり、 自分の汎変数を作る際の参考になる。
1: (defun alist-get (key alist) 2: "Get the value associated to KEY in ALIST." 3: (declare 4: (gv-expander 5: (lambda (do) 6: (macroexp-let2 macroexp-copyable-p k key 7: (gv-letplace (getter setter) alist 8: (macroexp-let2 nil p `(assoc ,k ,getter) 9: (funcall do `(cdr ,p) 10: (lambda (v) 11: `(if ,p (setcdr ,p ,v) 12: ,(funcall setter 13: `(cons (cons ,k ,v) ,getter))))))))))) 14: (cdr (assoc key alist)))
上記は一見複雑だが、重要なのは以下の3点である。
- 汎変数にしたい関数は、関数定義内で
(declare (gv-expander (lambda (do) …)))を宣言するか、または関数定義外で(put 'func 'gv-expander (lambda (do arg1 arg2 …) …))で汎変数関数を設定する。 [fn:: ~declare~ 経由で汎変数関数を設定する場合、 ~do~ 以外の引数の追 加は ~gv–defun-declaration~ が自動的に行なってくれる。] - 汎変数関数は、関数
doと 元関数の引数を引数にとる。そして汎変数関 数の中ではdoの引数の形で、setter処理を行う式を出力するコード とgetter処理を行う式を出力するコードを記述する。上記の例ではgetterは`(cdr ,p)で、setterは(lambda (v) `(if (….getter)))となる。その際、setterの中にgetterを記述して も構わない。 (gv-letplace (setter getter) place body)は、上記のように書かれた汎 変数関数に対して、placeリストの最初の要素の関数に対応する汎変数関 数のgetter関数とsetter関数を取り出して、それぞれをシンボルsettergetterに束縛する。
この方式は、かなり洗練されていて面白い。Emacsの汎変数がこのような形で設 計されているのは以下の理由による。
setterプログラムとgetterプログラムを同一関数内で記述することで、 構文スコープ下で両機能で変数を共有でき(クロージャ)、互いにネストし たり副作用を与えられる自由度の高いコードがかける。- マクロで記述しているので高速化ができる。特にEmacsの汎変数の根幹であ
る
gv-getマクロは事実上コンパイラになっている。
この do にどのような関数を入れるのかは、アプリケーションによって異な
る。たとえば setf は、 do 関数として、 setter/getter 両引数のうち
setter のみを呼び出して汎変数に引数の式を入れる。また gv-letplace
は、 do として引数の setter と getter をそのまま (setter
getter) に紐づける lambda 関数を使っている。
上記の例である alist-get は、cl.el の cl-assoc と同じ機能を持つ関
数である。 gv-expander で設定される関数は、関数 do, 位置 key, 構
造 alist を引数とする。
(macroexp-let2 macroexp-copyable-p k key …)
により、引数 key がコピーされて、 k に代入される。
(gv-letplace (setter getter) alist …) は、 alist の最初の要素(
当然 alist-get となる)に対応する汎変数関数(すなわち自分自身)
に対して、その値を操作する2関数 getter と setter (すなわち自分自
身の getter と setter プログラム)を取り出す。 getter は現在の
key での alist-get の値を返し、 setter は引数の値を alist に入
れる。
(macroexp-let2 nil p `(assoc ,k ,getter) … ) で、 p に alist の
k 対応値(Consセル)が入る。
setf が実行されると、 setf 内の do 関数は2つめのの引数の
setter を使って値を代入し、結果として得られる式を評価する。
以下は実行例。
(setq a '(("a" . "b") ("c" . "d"))) (alist-get "a" a) → "b" (setf (alist-get "a" a) "q") (alist-get "a" a) → "b" → "q"
Emacsの汎変数を記述する gv.el は、コンパイラやモナドに通じる興味深いコー ドとなっている。
Footnotes:
declare 経由で汎変数関数を設定する場合、 do 以外の引数の追
加は gv–defun-declaration が自動的に行なってくれる。