Emacs
EmacsDay 19

Emacs Lispの汎変数(とその他)

More than 1 year has passed since last update.

はじめに

Emacs Advent Calendar 19日目です。当初は Mac版EmacsのIMに関する新機能だけを紹介しようと思っていたのですが、kiwanamiさんの calfw.el の紹介の素晴らしさに当てられまして、追加で面白そうなEmacsの便利な機能を紹介したいと思います。ネタは3つです。

(小)ネタ1:emacs-24.4-mac-5.2 の新機能

Emacs 24.4 に対する山本氏のパッチ emacs-24.4-mac-5.2 には、新機能としてインプットメソッドへのAPIが整備されました。たとえば mac-selected-keyboard-input-source-change-hook は、ユーザがインプットメソッドを切り替える毎に呼ばれるフックです。以下に、日本語モードと英語モードでカーソルの色を変える例を示します。これで、入力モードを確認するのに、いちいち画面の右上に視線を移動しなくてもすみます。

  (when (fboundp 'mac-input-source)
    (defun my-mac-selected-keyboard-input-source-chage-function ()
      "英語のときはカーソルの色を黄色に、日本語のときは赤にします."
      (let ((mac-input-source (mac-input-source)))
        (set-cursor-color
          (if (string-match "\\.US$" mac-input-source)
              "Yellow" "Red"))))
    (add-hook 'mac-selected-keyboard-input-source-change-hook
              'my-mac-selected-keyboard-input-source-chage-function))

また、新機能 mac-auto-ascii-mode を有効にすると、ミニバッファにカーソルを移動する際、自動的に英語モードになります。これで日本語入力モードでうっかり M-x shell と入力しようとして M-x しぇっl になるような悲惨な事故をなくすことができます。

  (when (functionp 'mac-auto-ascii-mode)
    (mac-auto-ascii-mode 1))

(小)ネタ2:Emacs Lisp ファイルのorg-modeライクな折りたたみ

大きなソースファイル(init.el とか init.el とか init.el とか)を編集する際、見通しをよくするため、コードの一部をレベルに応じて折りたたみたい場合があります。

Emacs Lisp では、コメントの先頭のセミコロンの数に応じて、レベル が決まります。セミコロン3つが第1レベル、4つが第2レベル、5つが第3レベル…になります。 Emacs Lisp のバッファを outline-minor-mode にし、タブキーを org-cycle に割り当てることで、emacs lisp で org-mode のようなソースコードの折りたたみができるようになります。

以下に設定例を示します。(John Wiegley 氏の bind-key を使っています)

  (with-eval-after-load 'outline
    (bind-key "<tab>" 'org-cycle outline-minor-mode-map)
    (bind-key "C-<tab>" 'org-global-cycle outline-minor-mode-map)
    (bind-key "C-c C-f" 'outline-forward-same-level outline-minor-mode-map)
    (bind-key "C-c C-b" 'outline-backward-same-level outline-minor-mode-map)
    (bind-key "C-c C-n" 'outline-next-visible-heading outline-minor-mode-map)
    (bind-key "C-c C-p" 'outline-previous-visible-heading outline-minor-mode-map)
    (bind-key "<tab>" 'org-cycle outline-mode-map)
    (bind-key "S-<tab>" 'org-global-cycle outline-mode-map))

init.el を開く際に、自動的に折りたたんだ状態にするには、末尾に以下のようなコメントを加えます。

  ;; Local Variables:
  ;; outline-minor-mode: t
  ;; eval: (hide-sublevels 5)
  ;; End:

ネタ3:汎変数について

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

さらに、それらの関数に展開されるコンパイラマクロ (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 宣言された関数で、かつ未定義だった場合、自動的にライブラリを読み込ませることができます。

一般に、データ構造にアクセスする関数を汎変数にできます。以下に、Emacs 25 における汎変数に対応する関数を、データ構造毎に分類した一覧を示します。

汎変数に対応する関数の一覧

  • リスト・配列
    • 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系言語の大きな利点の一つです。 Emacs 24.3 の汎変数は、マクロと高階関数を活用することで、巧妙で、かつ分かり易い実装を実現します。gv.el は短いながらも妙味がありますので、興味のある方には眺めてみても良いかもしれません。

(追記: setf を除く汎変数マクロは現状では cl パッケージに入っているので、先頭に cl- 前置詞を付加しないと、コンパイル時に警告される可能性があります。)