Emacs Lisp には「汎変数」(general variable) という機能があります。従来はcl パッケージに含まれれていましたが、Emacs24.3 から 標準ライブラリへ移行しました。
汎変数とは何か
汎変数は、関数を変数的に扱える枠組みです。従来、変数 a
に 10 を代入する場合は、
(setq x 10)
としていました。汎変数は、変数を関数に拡張する枠組みです。関数 elt
を例にとりますと、 a
が (1 2 3)
である場合、 a
の2番目の要素は、
(elt a 1)
で取り出すことができます。この a
の2番目の要素に 10 を設定したい場合、 setq
の代わりに setf
で以下のように指定します。
(setf (elt a 1) 10)
これで a
の値は (1 10 3)
になります。
この setf
は setq
の拡張と見ることができ、置き換えることができます。setq
では、 x
が変数でしたが、 setf
では、 (elt a 1)
全体が変数とみなされます。
マクロと特別形式
ところで、Emacs Lisp には、関数に類する機能として、マクロと特別形式(special form) があります。関数は実行時に評価されますが、マクロはコンパイル時に評価されて、式を出力します。また、実行時にはさらに式が評価されて結果を出力します。特別形式は、引数が評価されない場合があります。
たとえば setq
は特別形式で、奇数番目の引数は評価されません。(詳細はEmacs Lisp リファレンス・マニュアル 9.2.7 参照)
一方、 setf
はマクロです。式がどのようにマクロ展開されているかを確認するには、 macroexpand
を使います。上の例の setf
はマクロ展開すると以下のようになります。
(macroexpand '(setf (elt a 1) 10)) ↩
(let* ((v a)) (if (listp v) (setcar (nthcdr 1 v) 10) (aset v 1 10)))
( elt
の引数の a
が、一旦、 let*
文の冒頭で新変数 v
に退避され、それを評価する式が let*
文の中で展開されています。このようになっている理由は後で説明します。)
汎変数を扱うマクロとその例
汎変数はマクロであり、コンパイル時に適切な形に展開されます。
以下に汎変数マクロの一覧を示します。 push
, pop
を除き、マクロの末尾に f
が付きます。
マクロ名 | 変数型 | 説明 |
---|---|---|
setf | 任意 | setq と同様、汎変数に値を設定する |
incf | 数値 | 汎変数の値を1(または任意数)増やす |
decf | 数値 | 汎変数の値を1(または任意数)減らす |
remf | plist | 汎変数plist からシンボルの値を除去する |
shiftf | 任意 | 汎変数群の値を1つずつずらす |
rotatef | 任意 | 汎変数群の値をローテートする |
callf | 任意 | 汎変数の値に関数を適用する |
callf2 | 任意 | 汎変数の値に関数を適用する(関数の第二引数) |
letf | 任意 | 一時的に汎変数の値を変更して評価する |
letf* | 任意 | 一時的に汎変数の値を変更して評価する |
getf | plist | 汎変数plistからシンボルの値を取得する |
push | リスト | 汎変数リストの先頭に要素を追加 |
pushnew | リスト | 汎変数リストの先頭に新要素を追加 |
pop | リスト | 汎変数リストの先頭から要素を取得 |
以下に elt
と配列を使った例を示します。多くは、汎変数を使わないとより複雑になります。
;; シンボルに代入する際は、setqとsetf は同じです。
(setf a [1 "hoge" (1 2 3 4 5)]) ↩
[1 "hoge" (1 2 3 4 5)]
;; 配列の先頭の要素を1増加させます。
(incf (elt a 0)) ↩
a ↩
[2 "hoge" (1 2 3 4 5)]
;; 配列の2番目の文字列の末尾に "page" を追加します。
(callf concat (elt a 1) "page") ↩
a ↩
[2 "hogepage" (1 2 3 4 5)]
;; 配列の3番目のリストから偶数を削除します
(callf2 remove-if 'evenp (elt a 2)) ↩
a ↩
[2 "hogepage" (1 3 5)]
;; 配列の3番目のリストの先頭を100にします。
(setf (car (elt a 2)) 100) ↩
a ↩
[2 "hogepage" (100 3 5)]
;; 配列の最初と最後を入れ替えます。
(rotatef (elt a 0) (elt a 2)) ↩
a ↩
[(100 3 5) "hogepage" 2]
;; 一時的に、配列の3つめを999にします。
(letf (((elt a 2) 999))
(format "%s" a)) ↩
"[(100 3 5) hogepage 999]"
a ↩
[(100 3 5) "hogepage" 2]
;; "hogepage" の最初の "ge" を "moge" に置き換えます。
(setf (substring (elt a 1) 2 4) "moge") ↩
a ↩
[(100 3 5) "homogepage" 2]
;; 配列の1つめのリストの先頭に10を追加します。
(push 10 (elt a 0)) ↩
a ↩
[(10 100 3 5) "homogepage" 2]
汎変数マクロ callf
, callf2
はもっとも基本となるマクロです。たとえば (setq a (concat a ".x"))
と (callf concat a ".x")
が同等、または (setq a (concat "x." a))
と (callf2 concat "x." a)
が同等です。
汎変数に対応する関数
上記の例に示した elt
や substring
等を含め、リストや文字列などにアクセスする多くの標準関数が、汎変数に対応しており、上述のマクロと組み合わせることで、可読性の高いコードを記述できます。
また、マクロ defstruct
で定義される構造体メンバや、関数のコンパイラマクロ (compiler-macro
) も汎変数として適用できます(エイリアスや置換含む)。
汎変数に対応している関数は、関数名シンボルの gv-expander
プロパティにラムダ関数が設定されています。以下に例を示します。
(プロパティの詳細については、ELisp リファレンス・マニュアルの 8.4節を参照ください。)
(pp (function-get 'elt 'gv-expander t)) ↩
(closure
(t)
(do &rest args)
(gv--defsetter 'elt
(lambda (store seq n)
`(if (listp ,seq)
(setcar
(nthcdr ,n ,seq)
,store)
(aset ,seq ,n ,store)))
do args))
ここで、単なる get
ではなく、 function-get
を使うと、シンボルがautoload
宣言された関数で、かつ未定義だった場合、自動的にライブラリを読み込ませることができます。
一般に、データ構造にアクセスする関数を汎変数にできます。以下に、Emacs25 における汎変数に対応する関数を、データ構造毎に分類した一覧を示します。
汎変数に対応する関数の一覧
-
リスト・配列
- aref
- caar
- cadr
- car
- cdar
- cddr
- cdr
- cl-eighth
- cl-fifth
- cl-ninth
- cl-seventh
- cl-sixth
- cl-subseq
- cl-tenth
- elt
- nth
- nthcdr
-
シンボルのプロパティ
- cl-get
- get
-
文字列
- substring
-
バッファ
- buffer-file-name
- buffer-local-value
- buffer-modified-p
- buffer-name
- buffer-string
- buffer-substring
- current-buffer
- current-column
-
face
- documentation-property
- face-background
- face-background-pixmap
- face-font
- face-foreground
- face-underline-p
-
キーマップ
- current-global-map
- current-input-mode
- current-local-map
- global-key-binding
- keymap-parent
- local-key-binding
-
ウィンドウ
- current-window-configuration
- window-buffer
- window-dedicated-p
- window-display-table
- window-height
- window-hscroll
- window-parameter
- window-point
- window-start
- window-width
- selected-window
-
オブジェクト配列
- default-value
- symbol-function
- symbol-plist
- symbol-value
-
alist
- alist-get (注:Emacs 25 より対応)
-
ファイル
- default-file-modes
- file-modes
- visited-file-modtime
-
フレーム
- frame-height
- frame-parameter
- frame-parameters
- frame-visible-p
- frame-width
- screen-height
- screen-width
- selected-frame
- selected-screen
- terminal-parameter
-
レジスタ
- get-register
-
環境変数
- getenv
-
ハッシュテーブル
- gethash
-
ポイント・マーカ
- mark
- mark-marker
- marker-position
- point
- point-marker
- point-max
- point-min
-
オーバレイ
- overlay-end
- overlay-get
- overlay-start
-
プロセス
- process-buffer
- process-filter
- process-get
- process-sentinel
-
プログラム構造・特殊形
- apply
- cond
- cons
- eq
- if
- let
- let*
- logand
-
その他
- gv-deref
- match-data
- mouse-position
- read-mouse-position
- standard-case-table
- syntax-table
- x-get-secondary-selection
以下はEmacs標準の各パッケージで定義される汎変数です。
- frameset.el
- frameset-prop
- scroll-bar.el
- get-scroll-bar-mode
- image-mode.el
- image-mode-window-get
- mailheader.el
- mail-header
- quickurl.el
- quickurl-url-comment
- quickurl-url-url
- url/url-parse.el
- url-port
- winner.el
- winner-active-region
汎変数の中には、興味深いものもあります。たとえば、 (setf (eq a 4) t)
では、 a
に 4 が代入されます。このような形で引数に関与するような汎変数は、従来の CL パッケージ define-setf-expander
で提供される手法では実現は困難でした。
汎変数マクロの例
汎変数を利用するマクロの例として、もっとも単純な setf
および callf
の実装を示します。(実際は繰り返し処理等があるのでもう少し複雑です。)
(defmacro setf-simple (place val)
(gv-letplace (getter setter) place
(funcall setter val)))
(defmacro callf-simple (func place val)
(gv-letplace (getter setter) place
(funcall setter (list func getter val))))
マクロは(実行時ではなく)コンパイル時に評価され、式を返します。通常の関数とは異なり、「実行時にどのような処理をさせる『式』を出力するのか」を記述します。
マクロ gv-letplace
は、 place
に対応する汎変数のラムダ関数をgv-expander
シンボルから取得します。また、本体 (body) では、2変数(ここでは getter
および setter
) に対して、汎変数を処理する式を生成するプログラムを書きます。
変数 getter
には、汎変数の値を取得する式が、変数 setter
には、「設定させたい値が出力される式」を引数にして呼び出すことで、実行時に値を設定する式を返す関数が入ります。(分かりにくい場合は、上述のcallf-simple
の実装を確認ください。)
(マクロ rotatef
等は、マクロの中で一時変数等を作りますが、静的スコープで動作するため、これらの変数は funcall
で呼ばれる setter
には影響を与えません。)
この仕組みによって、「データ構造にアクセスする式の生成」(汎変数)と、「データを処理する式の生成」(汎変数マクロ)が分離され、お互いが協調することで、汎変数を処理する式の全体が生成されます。
汎変数の定義方法
汎変数を定義するには、関数としての実装の他に、コンパイル時に評価される関数を関数名シンボルの gv-expander
プロパティに put
します。 elt
の例は上に示した通りです。
汎変数は非常に柔軟な設定ができますが、単純に設定用の式をマクロとして入れる場合は、 gv-define-setter
という便利なマクロが用意されています。
先ほどの elt
の例を以下に示します。
(gv-define-setter elt (store seq n)
`(if (listp ,seq) (setcar (nthcdr ,n ,seq) ,store)
(aset ,seq ,n ,store)))
これは、配列 seq
の n
番目に store
を設定する式がクォートされたものです。 gv-define-setter
は、この式に対して、『汎変数の引数の式を「評価し、 let
文のローカル変数に退避する式」を生成し、そのローカル変数の値を n
に代入した形で上記の式を展開する関数』を生成して関数のgv-expander
プロパティに putします。 (let
文の生成にはgv--defsetter
という補助関数が使われます。上記の例を参照。)
多くの汎変数は gv-define-simple-setter
や gv-define-setter
で設定できますが、マクロや特別形式、または上述の eq
のような、引数に配慮が必要な式の汎変数には、手動で gv-expander
を記述するものもあります。
汎変数の仕組み
ここでは、 nthcdr
関数の gv-expander
プロパティ値を確認してみましょう。
(pp (function-get 'nthcdr 'gv-expander t)) ↩
(closure
(t)
(do n place)
(macroexp-let2 nil idx n
(gv-letplace (getter setter) place
(funcall do `(nthcdr ,idx ,getter)
(lambda (v)
`(if
(<= ,idx 0)
,(funcall setter v)
(setcdr (nthcdr (1- ,idx) ,getter) ,v)))))))
オブジェクト closure
は、文脈スコープにおいて定義されるラムダ関数を表現するオブジェクトで、第三要素の (do n place)
が引数になります。
この gv-expander
プロパティに入れられた関数の引数 do
が汎変数の肝となります。Emacs Lisp の型を、
- 変数名: 型
- 関数名: ((引数1, 引数2, …) -> 返値)
で表現すると、 gv-expander
の型は以下のように表現されます。
gv-expaner: (do: ((getter: Exp, setter: ((store: Exp) -> Exp)) ->Exp) -> Exp)
これを外から分解してみると、
- 関数
gv-expander
は、do
という関数を引数に取り、式を返す。 - 関数
do
は、getter
という式と、setter
という関数を引数にとって、式を返す。 - 関数
setter
は、store
という式を引数にとって、式を返す
という3段構造になっています。「汎変数を利用する側」は、「どう利用するか(式を生成するか)」を do
に入れ、それを汎変数側に渡します。「汎変数側」は、 do
に対して、汎変数の値を取り出す式 (getter
) と、汎変数に値を設定するための式を生成する関数 (setter
) を提供することで、全体の処理式を生成します。
この do
の部分を、分かりやすく形式化したものが、 gv-letplace
関数です。 gv-letplace
は、引数 (getter
, setter
) および本体 body
をもとに、 (lambda (getter setter) body)
というラムダ関数を生成し、これを do
として、 place
の gv-expander
から取り出された関数に適用することで、式を作り出します。
前出の macroexp-let2
関数は、コンパイル(マクロ展開)時に出力される式において、引数が副作用を起こさないように、一旦、引数を評価して let
文のローカル変数に保存するための式を生成します。たとえば、
(incf (gethash (somefunc) table)
という式を考えてみます。 incf
は、汎変数から値を取り出し、1を加えて再びそれを汎変数に設定するので、都合2回、汎変数 (gethash ...)
を呼び出します。その際、 gethash
の中にある (somefunc)
が2回、式として展開されるならば、それは評価時にプログラマの意図しないものとなるでしょう。
そのため、汎変数の引数は1度評価したらその結果を別変数に保存するためにlet
文を生成し、その中で局所定義された変数を利用した式を生成します。
また、 gv-expander
の中でも、 gv-letplace
が利用されていることにも注目下さい。このように、汎変数の引数にも汎変数が使えるため、ネストして汎変数が利用できます。
Emacs 24.2 より前は、汎変数は cl パッケージにおけるdefine-setf-expander
によって定義されていました。ここでは、上記の目的を達成するために、(1) 汎変数の引数式を処理する式、 (2)その結果を一時的に代入する変数、そしてその変数を使って汎変数の値を (3) 取得したり(4) 設定したりする式、(5) 実行結果を格納する変数、の総計5つをばらばらに指定していました。これは、わかりにくい上に不自由な構造になっていました。
Emacs 24.3 から導入された gv.el は、クロージャと高階関数を活用することで、コンパイル時に汎変数の引数も汎変数側で自由に処理できるようにしています。
最後に
Lispのマクロは、関数とは異なる次元でコードの抽象度を高める興味深い機能です。コンパイル時に評価される式と、実行時に評価される式の両方を同じ言語で継ぎ目なく書けるのは、Lisp系言語の大きな利点の一つです。 Emacs24.3 の汎変数は、マクロと高階関数を活用することで、巧妙で、かつ分かり易い実装を実現します。gv.el は短いながらも妙味がありますので、興味のある方には眺めてみても良いかもしれません。
(追記: setf
を除く汎変数マクロは現状では cl パッケージに入っているので、先頭に cl-
前置詞を付加しないと、コンパイル時に警告される可能性があります。)