Emacs Lisp(基礎編)
著者 井上誠一郎
■■■ 想定読者
この章ではEmacs Lisp(以下,elisp)の説明をします.
想定読者は,Javaを一応知っているelisp初心者です.適宜,Javaの概念やコード例を引き合いにしながらelispの説明をします.
elispを知らないJavaプログラマにとって,elispの印象はおそらく次のようなものだと思います.
* カッコが多くて読みづらい
* 独特の用語が多くて難しい
* 関数型言語に違いないので,(たぶん)難しい
一方,関数型プログラミングの支持者から見るとelispの印象は次のようになると思います.
* 副作用が多すぎる
* ダイナミックスコープ(結果的にクロージャが無い)が時代遅れ
* (末尾最適化がないため)事実上,再帰で書けない
* 関数型言語のはずがない
双方から見て悪い面ばかり書きました.elispをプログラミング言語として見ると,古い印象がぬぐえないのは事実です.しかし,Emacsさえあれば,開発も実行もできる事実は大きな利点です.Lispの派生言語は数多くありますが,普及度の観点で,elispは他に圧倒的な差をつけています.
この章を読んでいる人は,おそらく自分のPCにEmacs系エディタ(以下,Emacs系エディタを総称してEmacsと表記します)をインストールしていると思います.これから示すサンプルコードをEmacsに打ち込んですぐに実行できます.開発環境のインストールや環境変数の設定など余計な手間はありません.この手軽さは,Javaの開発にはありません.
■■■ 開発環境および実行環境
必要なものはEmacsのみです.誤解を恐れずJavaプログラマ向けに言ってしまうと,EmacsがAll-In-One Eclipse相当です(脚注).コーディング,実行,デバッグ,検索,CVSやSubversionへのアクセス,ヘルプ参照,コード補完,とEclipseでできそうなことはEmacsでなんでもできます.できなければ,elispでコードを書けばできるようになります.
<脚注>
EmacsをEclipse程度と比較しないでくれと,Emacsファンから抗議がきそうです.許してください.
</脚注>
本章のサンプルコードはDebian etch上のEmacs21.4で動作確認しました.これから説明する基礎的な部分は,他のヴァージョンや派生エディタ(XEmacsやMeadowなど)でも動くと思います.
■■ elisp開発環境の整備
elispコードの動作確認の最も一般的な手段は,Emacsの*scratch*バッファ(以下,scratchバッファ)を使うことです.特別な設定無しでEmacsを起動すると,最初に現れるバッファです.Emacsの起動後は,C-x b でscratchバッファに切替えることができます.
scratchバッファのメジャーモードはlisp-interaction-modeです.このモードで使う便利なキー操作を以下にまとめます.本記事は,lisp-interaction-modeでコードを書く前提で説明します.
----------------------
<表>1 lisp-interaction-modeの便利なキー操作
----------------------
C-jの動作が気に入らない場合,emacs-lisp-modeを使うのが良いでしょう.式の評価をC-x C-eで行うと,評価結果がミニバッファに表示されます.ミニバッファはすぐに上書きされてしまうので,*Message*バッファを別ウィンドウに表示しながら実行することを勧めます.
一般に,scratchバッファはファイルに保存されません.最初のうちは,打ち込んだelispの実験コードをファイルに保存することを勧めます.例えばstudy.elを準備して,そこに実験コードを書き足していきます.標準設定では.elの拡張子のファイルはemacs-lisp-modeになります.lisp-interaction-modeにしたい場合,ファイルの先頭行に ; -*- lisp-interaction -*- と書いてください.
ヘルプを見たい場合は,調べたい文字列の上で,M-x describe-functionやM-x describe-variableを実行してください.*Help*バッファにヘルプが表示されます.ヘルプバッファをそのまま残しておきたいことは良くあります.その場合,M-x clone-bufferとして*Help*バッファを複製します.
■■■ elispのオブジェクト
elispの説明は,関数やリストから始めることが多いようです.本記事は,Javaプログラマに馴染みの深いオブジェクトから説明を始めます.
オブジェクト指向の教科書的には,「自分自身の振る舞いを知っているのがオブジェクト」と定義します.この観点からすると,elispのオブジェクトはややオブジェクトから離れます.しかし,elispのオブジェクトは自分自身の型を知っているので,広義ではオブジェクトと呼んで差し支えないでしょう.
オブジェクトの振る舞いを知っているのは,そのオブジェクトを引数として受け取る関数の側です.この点で,elispをオブジェクト指向言語と呼ぶのはおそらく無理があります.
■■ オブジェクト生成
リスト1のようにscratchバッファに文字列や数値を入力してC-jをタイプすると,入力文字列や数値がバッファにそのまま表示されます.
----------------------
<リスト1> 文字列オブジェクトや数値オブジェクトの生成
----------------------
一見,入力値がエコーバックされただけに見えますが,表示されたものは式の評価結果です."foo"や42はそれだけで式になります.そして式の評価結果はそれぞれ"foo"と42です.
Javaとの比較で比喩的に言えば,"foo"という式を評価すると,内部的には new String("foo") が起きるイメージです.同様に42という式を評価すると,内部的には new Integer(42) が起きるイメージです.
ここで重要なことは,オブジェクトは本質的に名前がない,ということです.Javaで new String("foo") や new Integer(42) でできる「オブジェクト」そのものには名前がないのと等価です.Javaの場合,オブジェクトを変数に代入することで,名前の無いオブジェクトを名前で呼べるようになります.elispは後述するシンボルを使って名前で呼べるようにします.
■■ オブジェクトの型
オブジェクトは型を持ちます.オブジェクト指向風に言えば,オブジェクトは自分自身の型を知っています.ここはJavaと同じです.
オブジェクトの型はtype-of関数で確認できます(リスト2).Javaで言えば,Object::getClass()に相当します.
----------------------
<リスト2> type-of関数の結果
----------------------
リスト2の'(クォート文字)は,今はおまじないだと思ってください(後で説明します).
elispにある型の一覧はEmacsのソースコードを見るのが確実です.data.cのtype-of関数の実装を見ると,リスト3に示す型があります.
----------------------
<リスト3> elispの型の一覧
----------------------
■■ ユーザ定義型
elispは,Javaのようにユーザ定義型を作れて,そのインスタンス化でオブジェクトを作れるのとは様子が異なります.Javaのクラス定義やインターフェース定義に相当する機能は,elispには言語仕様上ありません.
便宜上,ユーザ定義型に相当するコードは次のように実現します.ある特定の性質を持ったオブジェクトを入力(引数)として受け付ける関数群を定義します.これらの関数群により,オブジェクトの集合は,共通の振るまいを持ちます.結局,これが,オブジェクト集合が属する一種の「型」になります.一般には,オブジェクトの型を判定して真偽値を返す述語関数(後述)も提供します.
ring.elから抜粋したコードを示します.make-ring関数が,いわばコンストラクタ相当です.make-ring関数が返すオブジェクトはring型です.そして,ring.el内にはring型オブジェクトを引数として受け入れる関数群が定義されています.
----------------------
<リスト4> ring.elの一部抜粋
----------------------
残念ながら,ring型は言語仕様として提供された型ではありません.make-ringが返すオブジェクトをtype-of関数で調べてもringは返りません(consが返ります).また,ringオブジェクトを受け入れる関数に,ringオブジェクト以外を渡さないように書くのはプログラマの責任です.型定義と静的型に慣れたJavaプログラマには,馴染み辛い部分かもしれません.
■■■ シンボル
リスト5のようにsetq関数(脚注)を使うと,シンボルfooが文字列オブジェクト"FOO"を参照します(脚注).その後,シンボルfooだけの式を評価すると文字列"FOO"が返ります.また,同じシンボルfooで数値オブジェクトを参照することも可能です.
----------------------
<リスト5> シンボル
----------------------
この結果を見ると,シンボルはJavaでの変数相当,と予想できるかもしれません.実際,elispの入門書によっては,シンボルを変数のようなもの,と説明することがあります.シンボルをJavaの変数相当と思って,だいたいコードが読めるのも事実です.しかし,正確にはシンボルとJavaの変数は異なります.違いを明確にするために,シンボルを内部動作を含めて説明します.
<脚注>
正確には,setqは関数ではなくスペシャルフォームと呼ばれます.関数とスペシャルフォームの違いは引数を評価してから渡すか否かの違いです.スペシャルフォームは関数とは別物ですが,本記事では分かりやすさを優先して関数と呼びます.
</脚注>
<脚注>
このようなシンボルとオブジェクトの参照関係を束縛と呼びます.束縛を参照と呼び変えて大きな問題はないので,本記事では参照と呼びます.
</脚注>
■■ シンボルの内部動作
シンボル自体がオブジェクトです.シンボルの型定義は,Emacsのソースコードのlisp.hにLisp_Symbol構造体としてあります.リスト6は,この構造体定義をJavaのクラス風に書いた疑似コードです.Javaのフィールド相当をセルと呼ぶのはLisp用語です.セルをフィールドと読み替えて特に問題はありません.
----------------------
<リスト6> シンボルの型定義の疑似コード
----------------------
シンボルが生成されると,内部的にはこのLisp_Symbol型のインスタンスが生成されます.
(setq foo "FOO") で,"FOO"がJavaでの new String("FOO") 相当だとすれば,fooは new Lisp_Symbol("foo") 相当のことが起きています.リスト6の疑似コンストラクタを見て分かるように,Lisp_Symbolのnameフィールドの値がシンボル名"foo"になります.
シンボルfooが文字列オブジェクト"FOO"を参照する内部動作は,Lisp_Symbolのvalueフィールドが文字列"FOO"オブジェクトを参照する関係,と説明できます.リスト6の値セルの型がObjectであることから推測できるように,値セルはelispのあらゆる種類のオブジェクトを参照可能です.
シンボルの内部動作を理解するには,obarray(object arrayの略)と呼ばれるハッシュテーブルを知る必要があります.Java風に言えば,obarrayは Map<String,Symbol> です.obarrayのキーはシンボル名,値はシンボル型オブジェクトです.fooシンボルが生成されると,文字列"foo"をキーにしてLisp_Symbolオブジェクトがobarrayにputされます(脚注).
<脚注>
obarrayへのput操作を伝統的にinternと呼びます.internをputと読み替えて問題はないので,本記事ではobarrayへのputと呼びます.厳密に言うと,シンボル生成とinternは独立していますが,当面,同一視して問題はありません.
</脚注>
ここまでの動作を図示すると次のようになります.
----------------------
<図1> シンボルの内部動作
----------------------
シンボルが初めて評価されると,シンボルオブジェクトが生成されてobarrayにputされます.2度目以降の評価では,シンボル名をキーにしてobarrayからシンボルオブジェクトをgetします.シンボルもオブジェクトであることから,シンボルを参照するシンボルも定義できます.
以上から,Javaの変数に最も近いものを敢えて選ぶとしたら,シンボルそのものより,obarrayのキーの文字列(シンボル名)の方だと言えます.
■■ シンボルの評価とクォート
いくつかの例外を除いて,式の中にシンボルが現れると,そのシンボルは評価されて結果を返します.
後述するように,リストの先頭にシンボルを書いた場合,そのシンボルの評価値は,関数セルが参照する関数の戻り値になります.それ以外の場合,シンボルを評価すると,値セルが参照するオブジェクトを返します.シンボルの評価ルールはこれだけです.
シンボルを評価しないように明示的に指示することを,クォートと呼びます.'fooのように'(シングルクォーテーション)文字で指定できます.なお,setqのqはquoteのqです.第一引数は評価されずにsetq関数に渡っています.
クォートの動作を,backquoteと合わせて,実行例で示します.
----------------------
リスト7 シンボルのクォート
----------------------
■■ シンボルの解析
次のようにシンボルを解析できます.Java風に言えば,symbol-nameやsymbol-valueは,リスト6の疑似クラスのgetterメソッド相当です.
----------------------
リスト8 シンボルの解析
----------------------
■■ 特別なシンボル(tとnil)と述語(predicate)関数
tとnilという特別なシンボルがあります.それぞれ真と偽を表します.動作はリスト9を参照してください.
----------------------
リスト9 特別なシンボル(tとnil)と述語関数
----------------------
慣例として,真偽値を返す関数の名前にはpもしくは-pを付けます.Javaであれば boolean isSymbol(Object obj) と命名するところで,symbolpという命名にします.
■■ シンボルのまとめ
ここまでで分かったことをまとめます.
- シンボルは名前を持ちます (symbol-name関数で確認可能)
- シンボルの値セルは任意のオブジェクトを参照できます (symbol-value関数で確認可能)
- シンボル自身の型をtype-ofで確認すると symbol が返ります
まだ説明していないシンボルの性質には次のふたつがあります.
- シンボルの関数セルは任意の関数を参照できます (symbol-function関数で確認可能)
- 属性リストを持ちます.属性リストに関しては,put関数やget関数のヘルプを参照してください.
■■■ コンスセルとリスト
コンスセルは,ふたつのオブジェクトへの参照を持ったオブジェクトです.Emacsのソースコードのlisp.hで,Lisp_Cons構造体として定義されています.Java風の疑似コードにしてもあまり変わらないので,C言語のまま定義を載せます.
----------------------
リスト10 コンスセル構造体(lisp.hから)
----------------------
Lisp_ObjectはJavaでのObjectと考えてください.つまり,carとcdrのふたつのフィールドはelispの任意のオブジェクトを参照できます(脚注).
<脚注>
carとcdrのフィールドは伝統的にcarスロットとcdrスロットと呼ばれます.フィールドと読み替えて問題はないので,本記事ではフィールドと呼びます.
</脚注>
後述するように,コンスセルのcdrが別のコンスセルを指すことで,リスト構造を作ります.コンスセルで作るリスト処理こそがLisp(LISt Processing)の名前の由来でもあります.リストを見る前に,コンスセルをもう少し見ます.
■■ コンスセルの生成
コンスセルの生成の第一の方法は,("foo" . "bar") のように書くことです.
第二の方法はcons関数を使う方法です.両方を合わせてコード例と内部動作を示します(脚注).
----------------------
リスト11 コンスセルの生成
----------------------
<脚注>
consはconstructの略です.また(X . Y)の記法をdotted pair notationと呼びます.
</脚注>
■■ コンスセルの操作
carとcdrという関数でコンスセルの内部にアクセスします.carとcdr以外にコンスセルの中を参照する手段はありません.Java風に言えば,carとcdrはgetterメソッド相当です.対するsetterメソッド相当の関数はsetcarとsetcdrです.
setcarとsetcdrは破壊的な関数なので,教条的に言うと使うべきではない関数です.しかし,elispは教科書の中の言語では無いので紹介します(脚注).
----------------------
リスト12 コンスセルの操作(リスト11の続き)
----------------------
<脚注>
破壊的な関数を一切使わないと,効率が落ちる可能性がありえます.
</脚注>
■■ コンスセルのまとめ
ここまでで分かったことをまとめます.
- ふたつのフィールド(スロット)を持ちます.
- フィールドは任意のオブジェクトを参照できます.
- carとcdrという(アクセサ)関数でフィールドが参照するオブジェクトを取り出せます.
■■ リスト
コンスセルのcarとcdrのフィールドは,任意のオブジェクトを参照できます.コンスセルもオブジェクトの一種です.cdrが別のコンスセルを参照することを考えます.
cons関数でcdrがコンスセルを参照するコンスセルを生成するコード例を示します.
----------------------
リスト13 リストの生成
----------------------
後尾のコンスセルのcdrがnilを参照するようにします.こうしてできた ("FOO" "BAR") こそ,有名なリストです.以下に要素数が3つのリストの図とコード例を示します.
----------------------
図2 リストの内部図
----------------------
----------------------
リスト14 要素数が3つのリストの生成とcarとcdr
----------------------
ちなみに,nilと()(空リスト)は内部的にまったく等価です.
■■ リスト操作のイディオム
リスト操作には様々なイディオムがあります.コメントとともにコード例を示します.
----------------------
リスト15 リスト操作のイディオム
----------------------
■■ リストの評価
elispでは,式を評価して結果を返すのが動作の基本です.文字列や数値はそのままの値を返すので,あまり意識しませんが,内部的には評価して値を返しています.シンボルだけの式を評価すると,シンボルの値セルが参照するオブジェクトを返しました.
リストの評価は次のように行われます.
- リストの先頭要素(先頭のコンスセルのcar)のシンボルの関数セルの参照する関数を呼び出します
- リストの後続要素(先頭以外のコンスセルのcar)を関数の引数として渡します.引数はクォートされていなければ,評価してから引数に渡ります
リストの後続要素が,リストの場合もあります.この場合,後続要素(内側)のリストを評価,つまり関数呼び出しをしてから,外側のリストの関数呼び出しをします.この具体例は次の関数の説明の中で行います.
■■■ 関数定義
最初に,最も分かりやすい関数定義の方法と呼び出し例を示します.
----------------------
リスト16 関数定義と呼び出し
----------------------
リスト16のmやnは仮引数です.関数の戻り値(=関数の評価結果)は,関数本体の最後の評価結果です.Javaのメソッド定義と,表記の違いを除けば,違いは次の2点です.
- 型が無い (戻り値の型と引数の型)
- returnが無い
リスト16のdefunを見て,関数に名前があると思うのは間違いです.既に,オブジェクトには本質的に名前が無いことを説明しました(Javaと同じ).関数も本質的に名前がありません.elispの関数はオブジェクトだからです.この点で,メソッドに本質的に名前があるJavaとは異なります.
defunは,シンボルを作って,その関数セルが関数定義を参照するようにしています.
シンボルの値セルの操作に,setqやsetがあったように,関数セルにはfsetがあります(fsetqはありません).defunの意味とfsetのサンプルコードをリスト17に示します(脚注).
----------------------
リスト17 defunの意味(リスト16の続き)
----------------------
<脚注>
subr(subroutineの略)は,Cで書かれた関数を意味しています.
</脚注>
■■ functionpから関数とは何かを知る
----------------------
リスト18 subr.elから抜粋
----------------------
elispにとって,「関数」とは次の4つのいずれかであることが分かります.
- subroutine (Cで書かれた関数)
- バイトコンパイルされた関数
- シンボルlambdaで始まるリスト
- 関数セルが空ではないシンボル
■■ lambda
functionpで分かるように,先頭要素がシンボルlambdaのリストは関数です.
(lambda (引数 ...) (関数本体)) が関数定義です.
""で囲った文字列表現が文字列オブジェクトを生成し,数値表現が数値オブジェクトを生成するように,lambdaシンボルで始まるリスト表現は関数オブジェクトを生成します.
■■ 関数呼び出し
既に説明したように,リストの先頭要素に「関数」があれば,関数呼び出しになります.リストの残りの要素は関数に渡る引数です.引数は評価されてから関数に渡ります(脚注).
様々な関数呼び出しのコード例をリスト19に示します.
----------------------
リスト19 関数呼び出しコード例
----------------------
<脚注>
スペシャルフォームの場合,引数が評価されずに渡ります.
</脚注>
■■ letとは
ここまでに見たシンボルは,Emacsの全てのコードから参照可能です.いわゆるグローバルスコープです.シンボルの名前はJavaでの変数相当なので,グローバルスコープは言わばグローバル変数のようなものです(Javaにはグローバル変数はありませんが).
letを使うことで,明示的にローカルシンボルを作成できます.実用的なプログラムでは,多くのシンボルがローカルスコープです.letの使用例を以下に示します.
----------------------
リスト20 letの使用例
----------------------
■■ ダイナミックスコープ
elispはダイナミックスコープを採用しています(脚注).ダイナミックスコープとレキシカルスコープの違いを以下のコードで示します.
----------------------
リスト21 ダイナミックスコープ
----------------------
ダイナミックスコープでは,実行時にsetqなどでシンボルがオブジェクトを参照すると,常にその参照が有効になります(既存の参照関係を上書きします).letでローカルシンボルがオブジェクトを参照すると,同じ名前のシンボルの既存の参照関係を上書きます.let式を抜けると,既存の参照関係が復帰します.
リスト21で説明します.コードは書かれたままに評価されます.まずdefunを囲むlet式が評価されます.このlet内を実行する時,シンボルfooは"FOO"文字列オブジェクトを参照します.my-show-foo関数定義を抜けると,この参照関係は消滅します.
次にmy-show-fooの関数呼び出しを含むlet式を評価します.まずシンボルfooが文字列"BAR"を参照します.そして関数呼び出しをします.let式内なので,シンボルfooは文字列"BAR"を参照した状態でmy-show-fooが実行されます.戻り値はシンボルfooの評価結果なので,文字列"BAR"が返ります.
レキシカルスコープはJavaと同じです.ただし,今のJavaは,入れ子の関数,つまり関数の中に関数を定義できないため,リスト21のコードは必ずしも自明では無いかもしれません.
lexical-let式内のmy-show-fooの関数定義を評価する時,シンボルfooは文字列"FOO"を参照した状態で関数が定義されます.my-show-fooを呼ぶ時も,その参照関係は残ったままです.
<脚注>
Perlに馴染みのある人は,myがレキシカルスコープ,localがダイナミックスコープであることを知っていると思います.ダイナミックスコープは時代遅れ,というのが現代の一般的な評価のようです.
</脚注>
■■■ 制御構造
Javaと異なり,elispには文法としての制御構造はありません.以下に説明するifやwhileは予約語ではなく,シンボルです.elispの構文は式が並ぶだけです.文も演算子もありません.
構文としては,ifもwhileも関数呼び出しの式です.関数の場合,関数呼び出しの前に引数が評価されます.ifやwhileの場合,評価されずに渡る引数があります.これらは関数と区別するため,スペシャルフォームと呼ばれます.
以下のコード例ではJavaプログラマに馴染みの制御構造のみを書きました.文法として制御構造を持たない性格から,elispではプログラマ自身が独自の制御構造を作り上げることができます(脚注).このため,コード例に挙げた以外にも多くの制御構造があります.
----------------------
リスト22 制御構造のコード例
----------------------
<脚注>
マクロを使います.関数型言語の性質は,最近のプログラミング言語の多くが取り入れています.この点で,elispの特別さは薄れています.しかし,マクロによるメタプログラミングの部分は,いまだelisp(Lisp系言語)を特別なものにしている要因のひとつです.
</脚注>
■■■ その他のコンテナ型
elispのコンテナ型の基本はリスト型です.Javaのコレクション型で対応するのはArrayListです.他にもいくつかのコンテナ型があるので,ここでJavaの型との対応を示します.
■■ 連想リスト(association list)
インターフェースはMap相当です.名称からHashMap相当を想像するかもしれませんが,内部的にはキーと値のペアを要素とするリストです.そのため探索のオーダーはO(n)です.
■■ ベクタ
JavaでのObject[]相当です.オブジェクト生成後,要素の追加と削除はできません.要素の書き換えは可能です.
■■ ハッシュテーブル
JavaのHashMap相当です.Emacs21以降に追加された型です.