44
38

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

EmacsAdvent Calendar 2014

Day 19

Emacs Lispの汎変数

Last updated at Posted at 2014-12-19

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) になります。

この setfsetq の拡張と見ることができ、置き換えることができます。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) が同等です。

汎変数に対応する関数

上記の例に示した eltsubstring 等を含め、リストや文字列などにアクセスする多くの標準関数が、汎変数に対応しており、上述のマクロと組み合わせることで、可読性の高いコードを記述できます。

また、マクロ 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)))

これは、配列 seqn 番目に store を設定する式がクォートされたものです。 gv-define-setter は、この式に対して、『汎変数の引数の式を「評価し、 let 文のローカル変数に退避する式」を生成し、そのローカル変数の値を n に代入した形で上記の式を展開する関数』を生成して関数のgv-expander プロパティに putします。 (let 文の生成にはgv--defsetter という補助関数が使われます。上記の例を参照。)

多くの汎変数は gv-define-simple-settergv-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)

これを外から分解してみると、

  1. 関数 gv-expander は、 do という関数を引数に取り、式を返す。
  2. 関数 do は、 getter という式と、 setter という関数を引数にとって、式を返す。
  3. 関数 setter は、 store という式を引数にとって、式を返す

という3段構造になっています。「汎変数を利用する側」は、「どう利用するか(式を生成するか)」を do に入れ、それを汎変数側に渡します。「汎変数側」は、 do に対して、汎変数の値を取り出す式 (getter) と、汎変数に値を設定するための式を生成する関数 (setter) を提供することで、全体の処理式を生成します。

この do の部分を、分かりやすく形式化したものが、 gv-letplace 関数です。 gv-letplace は、引数 (getter, setter) および本体 body をもとに、 (lambda (getter setter) body) というラムダ関数を生成し、これを do として、 placegv-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- 前置詞を付加しないと、コンパイル時に警告される可能性があります。)

44
38
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
44
38

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?