[ << ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
defun
内の単語のカウント
次の計画の目標は、関数定義の中の単語の数を数えることである。当たり前のこ
とだが、これは count-words-region
の使い方をちょっと工夫すれば出
来てしまう。カウント:繰り返しと正規表現, を参照
のこと。例えば、ある一つの定義の中の単語数を数えたければ、C-M-h
(mark-defun
) コマンドを使って定義部分をマークしてから
count-words-region
を呼び出せばよい。
しかしながら、ここではもっと大きなことをやってみたいと思う。Emacs のソー スの中の全ての定義の中の単語とシンボルの数を数えて、そこにどれだけの関数 があり、各々がどのくらいの長さかをグラフにして出力するとか、40個から49個 までの単語とシンボルをもつ関数がどれだけあるか、50個から59個までではどうか、 といったことを調べるのである。私はしばしば典型的な関数というのがどのくらい の長さかを知りたくなる。これは、そういったことを教えてくれるものである。
はっきり言って、このヒストグラムを書く計画は人をひるませる類のものである。 しかし、これをいくつもの細かいステップに分けて、各々を一つずつ見ていくこ とにすれば、それほど恐れるほどのものではない。そこで、どんなステップに分 けるべきかを書いてみることにする。
count-words-in-defun
を使うことが出来る。
これはかなりの大計画である。しかし、各々のステップをゆっくりと進んでいけ ば、それ程困難なものではない。
関数定義の中の単語数を数えるにはどうしたらよいか、を最初に考え始めた時に、
まず疑問に思うこと (あるいは、考えるべきこと) は、我々は何を数えればよい
かということである。Lisp の関数定義に関して単語のことを話す場合、実際に
は大抵シンボルのことを言っている。例えば、次の multiply-by-seven
関数は、defun
、multiply-by-seven
、number
、*
、
そして7
という5個のシンボルを含んでいる。これに加えて説明文字列の
中に `Multiply'、`NUMBER'、`by'、そして `seven' とい
う単語が含まれている。`number' は繰り返して使われているので、関数定
義の中には合計10個の単語とシンボルが含まれていることになる。
(defun multiply-by-seven (number) "Multiply NUMBER by seven." (* 7 number)) |
ところが、もし multiply-by-seven
の定義を C-M-h
(mark-defun
) でマークして、そこで count-words-region
を呼
び出してみると、10個ではなく11個の単語があるという答えが帰ってくる。何かが
おかしい!
実は、問題は二重になっている。count-words-region
は `*' を単
語とは数えないが、逆に、一つのシンボル multiply-by-seven
を三つの
単語だと数えてしまうのである。これはハイフンが一つの単語内でのつながりを
示すものとしてではなく、単語間の間の空白と同じように扱われるためのである。
従って、`multiply-by-seven' は `multiply by seven' と書かれて
いるように扱われることになる。このような混乱の原因は、
count-words-region
の定義内で一つの単語ずつ移動する際に使っている
正規表現にある。標準的な count-words-region
のバージョンで使われ
ている正規表現は
"\\w+\\W*" |
である。この正規表現は一つ以上単語構成文字が続いた後に0個以上の非単語構 成文字が続くというパターンである。「単語構成文字」によって何が意味される かという問題は、構文 (syntax) の問題になる。これには一つのセクションを割 当てて論じる価値がある。
Emacs では、各々の文字はある構文カテゴリ (syntax categories) に属するものとして扱われる。例えば正規表現 `\\w+' は一つ以上の「単 語構成文字」(word constituent) が続くパターンを表している。単語構 成文字というのは、ある一つの構文カテゴリーの要素のことである。他の構文カ テゴリーの要素は、例えば終止符やカンマ等の句読点文字のクラス、スペースや タブ等の空白文字のクラスを含んでいる。(より詳しいことについては section `The Syntax Table' in The GNU Emacs Manual, 及 び section `Syntax Tables' in The GNU Emacs Lisp Reference Manual,を参照のこと。)
構文テーブルとはどの文字がどのカテゴリーに属するかを定めるものである。普
通、ハイフンは「単語構成文字」には分類されない。そうではなく「シンボルの
名前ではあるが、単語ではないものの一部をなす文字のクラス」に分類される。
これは count-words-region
関数がハイフンを単語間の空白文字と同じ
扱いをすることを意味する。これが count-words-region
が
`multiply-by-seven' を三つの単語だと数える理由である。
Emacs に multiply-by-seven
を一つの単語だと数えさせるには二つの方
法がある。一つは構文テーブルを書き換える方法、もう一つは正規表現を書き直
す方法である。
Emacs が各々のモードに対して持つ構文テーブルを書き換えることで、ハイフン を単語構成文字だと再定義することが出来る。この動作は我々の目的に殆ど合う のだが、ハイフンだけが単語の中には現れずシンボルの中には出てくる文字とい うわけではない。似たような文字は他にもある。
代わりに、count-words
の定義中の正規表現の方を書き直してシンボル
を含むようにすることも出来る。こちらの方法の方がより簡明である。ただし、
実際にやることは少々トリッキーだ。
最初の部分は十分に単純である。パターンとしては「少なくとも一つ以上続く、 単語ないしはシンボルの構成要素」にマッチするもの、つまり、
\\(\\w\\|\\s_\\)+ |
になる。`\\(' は `\\w' と `\\s_' のいずれかを表わす正規表 現のグループの開始を示す部分である。対象となる二つの部分は `\\|' で 区切られている。`\\w' は任意の単語構成文字にマッチし、`\\s_' はシンボル名の一部になり得るが、単語構成文字ではないような任意の文字にマッ チする。後に続く `+' は、このグループに属する文字、即ち単語かシンボ ルの構成文字が少くとも一回はマッチしなければならないことを意味する。
しかしながら、正規表現の二番目の部分はもっと難しい。欲しいものは、一番目 の正規表現に続けて「単語の一部にもシンボルの一部にもならない文字が一つ以 上続いてもかまわない」ことを表わす表現である。まず思い浮んだのは次のよう なものである。
\\(\\W\\|\\S_\\)*" |
上の大文字の `W' と `S' は各々単語、あるいはシンボルの構成文字 ではないような文字にマッチする。しかし、この表現では、単語構成文 字ではないか、もしくはシンボル構成文字ではない文字に一致してしまう。これ ではどんな文字にもマッチしてしまう。
次に私は、テストしているリージョン内の全ての単語やシンボルの後には空白文 字 (スペース、タブ、もしくは改行) があることに気がついた。そこで、単語か シンボルの構成文字が一つ以上続くというパターンの後に、一つ以上の空白文字 が続くというパターンを置いてみた。しかし、これも失敗した。通常は単語やシ ンボルは空白で終わるのだが、実際のコードでは、シンボルの後に括弧が来たり、 単語の後に句読点が来たりすることだってある。というわけで、結局、単語かシ ンボルの構成文字の後に0個以上の空白文字以外の文字が続き、その後に0個以上 の空白文字が来る、というパターンにすることにした。
次がその正規表現である。
"\\(\\w\\|\\s_\\)+[^ \t\n]*[ \t\n]*" |
count-words-in-defun
以前見たように、count-words-region
関数を書く方法は幾つかあった。
が、今回 count-words-in-defun
を書く際には、この内の一つの方法だ
けを採用することにする。
方法としては while
ループを使う方法が理解しやすいだろうから、こち
らを採用することにする。count-words-in-defun
は、より複雑なプログ
ラムの一部になるので、インタラクティブである必要も、メッセージを出す必要
もなく、ただカウントを返しさえすればよい。これらのことを考慮すると、定義
は少し単純になる。
一方、count-words-in-defun
は関数定義を含むバッファの中で使われる。
従って、現在ポイントが関数定義内にあるか判定し、もしそうであればその定義
内の単語を数えるというふうにするのが合理的であろう。こうすると、ちょっと
コードが複雑にはなるが、関数に引数を与える手間は省ける。
以上のことを考慮すると、テンプレートは次のようになる。
(defun count-words-in-defun () "説明文字列..." (初期設定... (while ループ...) カウントを返す) |
いつも通り、やるべきことはこの中の空きスロットを埋めていくことである。
まずは初期設定から。
この関数は、関数定義が含まれるバッファの中で呼び出されることを想定されて
いる。現在のポイントは関数定義の中にあるか、外にあるかどちらかである。
count-words-in-defun
が動作してくれるためには、ポイントが関数定義
の先頭に移動し、カウンタがゼロから始まり、ポイントが関数定義の最後に来た
らループが終了するようになっていてくれなければならない。
beginning-of-defun
関数は後方に向かって行頭の `(' などの開き
括弧を検索し、そこにポイントを移動するか、検索の限界まで移動する。実際に
は、beginning-of-defun
はポイントを現在ポイントが含まれている関数
定義もしくはポイント以前のポイントに最も近い関数定義の開始位置、あるいは
バッファの先頭まで移動することになる。従って、beginning-of-defun
を使うことで望みの位置までポイントを移動することが出来る。
while
ループでは、数えた単語やシンボルの数を保持しておくカウンタ
が必要である。let
式によって、この目的のための変数を作り、その値
をゼロに初期化することが出来る。
end-of-defun
関数は beginning-of-defun
と同じような働きを
するのだが、ポイントを関数定義の終了位置に移動する点だけが異なっている。
end-of-defun
は関数定義の終了位置を決定するS式の一部として使う
ことが出来る。
ということで、count-words-in-defun
の初期設定部分はあっさり書けて
しまう。まずは、関数定義の最初にポイントを移動し、次にカウンタのための局
所変数を用意し、最後に while
ループが止まるべき所で止まれるように
関数定義の終了位置を記録しておくのである。
コードは次の通りである。
(beginning-of-defun) (let ((count 0) (end (save-excursion (end-of-defun) (point)))) |
このコードは単純である。ちょっとややこしいのは end
に関するところ
だろう。これには、関数定義の終了位置がバインドされる。その際、
save-excursion
式の中で一時的に end-of-defun
で関数定義の
終了位置に移動した後にポイントの位置を返すという方法を用いている。
さて、count-words-in-defun
の初期設定に続く二番目の部分は
while
ループである。
このループでは、ポイントを単語やシンボルごとに移動するS式、及びジャンプ
の回数を数えるS式が必要である。また、while
ループの真偽テストで
は、ポイントがまだジャンプすべきなら真を返し、定義の終了位置まで到達した
なら偽を返すようなものであるべきである。目的のための正規表現は既に再定義
してしまっているので、(単語やシンボルは何から構成されてい るか, 参照) ループは簡単に書ける。
(while (and (< (point) end) (re-search-forward "\\(\\w\\|\\s_\\)+[^ \t\n]*[ \t\n]*" end t) (setq count (1+ count))) |
関数定義の三番目の部分は単語やシンボルの数を返す部分である。この部分は
let
式の本体部分の最後の部分だが、極めて単純に局所変数
count
を書いておくだけでよい。これを評価すると数が返るわけである。
以上をまとめると、count-words-in-defun
は次のようになる。
(defun count-words-in-defun () "Return the number of words and symbols in a defun." (beginning-of-defun) (let ((count 0) (end (save-excursion (end-of-defun) (point)))) (while (and (< (point) end) (re-search-forward "\\(\\w\\|\\s_\\)+[^ \t\n]*[ \t\n]*" end t)) (setq count (1+ count))) count)) |
これをテストするにはどうしたらよいだろうか。この関数はインタラクティブで
はないが、ちょっとS式をかぶせることで簡単にインタラクティブにすることが
出来る。これには count-words-region
の再帰関数版とほぼ同じコード
が使える。
;;; インタラクティブバージョン (defun count-words-defun () "Number of words and symbols in a function definition." (interactive) (message "Counting words and symbols in function definition ... ") (let ((count (count-words-in-defun))) (cond ((zerop count) (message "The definition does NOT have any words or symbols.")) ((= 1 count) (message "The definition has 1 word or symbol.")) (t (message "The definition has %d words or symbols." count))))) |
便宜上 C-c = というキーバインディングをもう一度使うことにしよう。
(global-set-key "\C-c=" 'count-words-defun) |
以上で、count-words-defun
をテストするための準備が整った。まずは、
count-words-in-defun
及び count-words-defun
を両方ともインス
トールして、キーバインディングの設定もしてしまおう。そして、カーソルを次
の定義の中に移動して実験してみる。
(訳註:原文とは異なり、インデントはしていない。(defun
が行頭から
始まっていないと beginning-of-defun
等がうまく働かないからである。)
(defun multiply-by-seven (number) "Multiply NUMBER by seven." (* 7 number)) => The definition has 10 words or symbols. |
成功だ! この定義の中には確かに10個の単語とシンボルがある。
次の問題は、一つのファイルの中の幾つかの定義にある単語とシンボルの数を 数えることである。
`simple.el' のようなファイルの中には80以上の関数定義が含まれていた りする。我々の最終的な目標は沢山のファイルについての統計を取ることである が、その最初のステップとして、まずは一つのファイルについての統計を取るこ とを目標にしよう。
情報は数の列の形で与えられ、各々の数は関数定義の長さになる。これらの数は リストの中に保持しておくことが出来る。
一つのファイルについての情報は最終的には多くのファイルについての情報の形 に統合されることになる。従って、ここで作成する一つのファイル内の関数定義 の長さを数える関数は単に「長さ」のリストを返すだけでよく、特にメッセージ とかを表示する必要はない。
単語を数えるコマンドには、単語ごとにポイントを前方に進めるS式とジャンプ の回数を数えるS式が含まれていた。定義の長さを測る関数も、同じように設計 することが出来る。この場合は定義ごとにポイントを進めるS式と長さのリスト を作成するようなS式が含まれることになる。
問題をこのように言い替えてしまえば、関数定義を書くのは簡単なことである。
明らかに、カウントはファイルの先頭から始めなければならない。従って、最初
のコマンドは (goto-char (point-min))
である。次に while
ルー
プに入る。ここでは、ループの真偽テストは次の関数定義を探す正規表現に取れ
る---検索が成功している間はポイントを進め、本体を評価するわけである。本
体内には長さのリストを作成するS式が必要である。これには cons
と
いうリストを構成するコマンドが使える。やるべきことの殆どは、以上で終わっ
ている。
部分的にコードを書くと次のようになる。
(goto-char (point-min)) (while (re-search-forward "^(defun" nil t) (setq lengths-list (cons (count-words-in-defun) lengths-list))) |
この他にやることは、関数定義を含むファイルを見つけることである。
今までの例では、この Info file を使うか、`*scratch*' バッファなどの 他のバッファに一旦戻って、また帰ってくるということしかしていなかった。
ファイルを見つける (find) ことは、この文書では初めて出てくるプロセスであ る。
まずは、find-file
のソースを見てみよう。(関数のソースは
find-tag
を使うと見つけることが出来る。)
(defun find-file (filename) "Edit file FILENAME. Switch to a buffer visiting file FILENAME, creating one if none already exists." (interactive "FFind file: ") (switch-to-buffer (find-file-noselect filename))) |
定義には、簡潔な説明文字列が付いており、インタラクティブ式では対話的に用
いた時のプロンプトが指定されている。で、定義の本体を見ると、
find-file-noselect
及び switch-to-buffer
という二つの関
数が使われている。
C-h f (describe-function
コマンド) で表示される説明によると、
find-file-noselect
は指定されたファイルをバッファに読み込み、その
バッファを返す。しかしながらバッファは選択されない、つまり Emacs は注意
をそのバッファには向けない。(あるいは find-file-noselect
を名前の
ついたバッファに対し使った場合は、あなたの注意も引かない。) この仕事は
switch-to-buffer
がやってくれる。この関数は、Emacs が注目するバッ
ファを指定するものである。更にこの関数は、ウィンドウに表示されているバッ
ファを新しいバッファに切り替える。バッファの切り替えについては、また別の
場所で議論することにしよう。(バッファ間の移動,
参照。)
今やろうとしているヒストグラム計画では、定義の長さを調べる際にいちいち一
つ一つのファイルをスクリーンに表示する必要はない。というわけで
switch-to-buffer
ではなく set-buffer
を使うことにしよう。
これも Emacs が注目するバッファを切り替えるのだが、スクリーンに表示する
バッファはそのままである。従って、我々の目的のためには find-file
は使えず、そのためのコードを書くことになる。
といっても、やることは簡単だ。単に find-file-noselect
と
set-buffer
を使えばよいのである。
lengths-list-file
についての詳細
lengths-list-file
関数の核心部分は、defun から defun へ移動してい
く関数を含む while
ループと、各々の defun の中に含まれる単語やシ
ンボルの数を数える関数である。そしてその周辺に、例えば、ファイルを見つけ
たり、ポイントが必ずファイルの先頭部分からスタートするようにしたりする、
といった他の様々な仕事をする関数が来る。結局、定義は次のようになる。
(defun lengths-list-file (filename) "Return list of definitions' lengths within FILE. The returned list is a list of numbers. Each number is the number of words or symbols in one function definition." (message "Working on `%s' ... " filename) (save-excursion (let ((buffer (find-file-noselect filename)) (lengths-list)) (set-buffer buffer) (setq buffer-read-only t) (widen) (goto-char (point-min)) (while (re-search-forward "^(defun" nil t) (setq lengths-list (cons (count-words-in-defun) lengths-list))) (kill-buffer buffer) lengths-list))) |
この関数は一つの引数を取る。これは作業対象となるファイルの名前である。 説明文字列は四行あるが、インタラクティブ宣言はされていない。本体の一行目 では、使った人が計算機が壊れたのではないかと心配しないように、最初に何を やっているかを表示するようにしている。
次の行で save-excursion
が使われているので、Emacs は仕事が終わっ
た後、ちゃんと元のバッファに注意を戻してくれる。こうすると、この関数を他
の関数の中に埋め込んでいる場合などにも、ポイントを元の位置に戻してくれる
ので便利である。
let
式の変数リストの所で、Emacs はファイルを見つけて局所変数
buffer
をそのファイルを中身とするバッファにバインドする。同時に
Emacs は lengths-list
を局所変数として生成する。
次に Emacs は注意をそのバッファに向ける。
次の行では Emacs はバッファを書き込み不可にしている。理論上は、この行は 不要である。関数定義内の単語やシンボルの数を数える関数の中で、バッファを 書き換えたりするようなものはないし、たとえそのバッファが変更されたとして も保存されたりはしない。こういう警戒をするのは、これらの関数が Emacs の ソース上で作業するために、万が一にでもファイルを修正してしまったりすると 非常に不都合であるという理由のためである。言うまでもないが、私自身は実験 が失敗して私の Emacs のソースファイルが修正されるなんていう事態に会わな い限り、この行が必要だと思うことはないだろう。
次に、バッファがナローイングされている場合には、それを広げるということを やっている。これは普通は必要ない---Emacs はそのファイルに対応するバッファ が無い場合は新規にバッファを開くからだ。しかし、既にある場合には Emacs はそのバッファを返す。この場合、もしそのバッファがナローイングされていた なら、それを解除する必要がある。真にユーザーフレンドリーな関数にしたい場 合には、ナローイングやポイントの位置なんかを保存しておくべきだろうが、こ こでは何もしないことにする。
(goto-char (point-min))
式でポイントをバッファの先頭に移動する。
そして while
ループが来る。ここで、この関数の仕事が実行されること
になる。このループでは、Emacs は各々の定義の長さを調べ、その長さのリスト
を作っていく。
あるバッファでの作業が終わると Emacs はそのバッファを kill する。これは、
Emacs 内部でのスペースの節約のためである。私が使っている Emacs 19 のバー
ジョンには300以上ものソースファイルが含まれており、これらに
lengths-list-file
が適用される。もし Emacs がこれら全てを読み込ん
で一つも kill しなかったら、私の計算機は仮想記憶を使い切ってしまうだろう。
終りまで来ると、let
式の中の最後のS式である lengths-list
という変数が評価される。この値が関数全体の値となる。
この関数をいつも通りインストールして試してみることが出来る。インストール
が終わったら、カーソルを次のS式の直後に持っていってから C-x C-e
(eval-last-sexp
) とタイプして、評価してみよう。
(lengths-list-file "../lisp/debug.el") |
(多分、ファイルのパス名を変更する必要があるだろう。上に挙げたものは、こ
の Info ファイルのあるディレクトリと Emacs のソースがあるディレクトリが
/usr/local/emacs/info
と /usr/local/emacs/lisp
の
ように隣同士にある場合だけである。変更する場合は、このS式を一旦
`*scratch*' バッファにコピーしてから、それを修正して評価する。)
私が使っているバージョンの Emacs では、`debug.el' に対する長さのリ ストを生成するのに7秒かかり、結果は次のようになった。
(75 41 80 62 20 45 44 68 45 12 34 235) |
ファイルの中の最後の定義の長さは、リストの最初に現れることに注意しよう。
前節では、各ファイルの中に含まれる各関数の長さのリストを返すような関数を 作成したのだった。今度は、ファイルのリストが与えられた時に、それらのファ イルの中の関数の長さのマスターリストを返すような関数を定義してみたい。
リストの中の各ファイルに対する作業は繰り返しの動作なので、while
ループや再帰を使って行うことが出来る。
while
ループを使った方法はルーティーンワークである。関数に渡す引
き数はファイルのリストになる。以前見たように
(ループの例, 参照)、
このリストが要素を含んでいる時のみループの本体を実行し、要素が無くなった
ら抜けるようにすることが出来るのであった。これがうまく動作するためには、
本体部分で、本体が一回評価されるごとにこのリストを短くしていき、結果とし
て最後にはリストが空になるように、S式を書いておく必要がある。このために
は、本体が評価されるごとに、リストにそのリストの CDR の値をセットす
るという技法を用いるのが普通である。
テンプレートは次のようになる。
(while リストが空かどうかのテスト 本体... リストを自分自身の cdr にセット) |
さて、while
ループは常に (真偽テストの結果として) nil
を返
し、本体内のS式の値を返したりすることはないのだった。(従って、ループの
本体のS式は副作用として評価される。) しかしながら、長さのリストをセッ
トするS式は本体の一部である---にもかかわらず、その関数全体の値として返
して欲しいのもこの値である。そこで、while
ループを let
式
で包んで、let
式の最後の要素が長さのリストの値を含むようにする。
(増加するカウンタを使ったループの例, を参
照。)
;;; |
expand-file-name
は組み込み関数であり、ファイル名を絶対パスも含
めた省略無しの形に戻すものである。従って、例えば
debug.el |
は
/usr/local/emacs/lisp/debug.el |
と展開される。
その他の、上の関数内に出てくる新しい要素は append
だけである。こ
れには一つのセクションを割当てた方が良いだろう。
14.7.1 関数 append
あるリストを別のリストに追加する
append
append
関数は、あるリストを、もう一つのリストに追加するものである。
例えば、
(append '(1 2 3 4) '(5 6 7 8)) |
の結果は次のようになる。
(1 2 3 4 5 6 7 8) |
lengths-list-file
によって作成された二つの長さのリストを一つにま
とめる際は、このような形になって欲しいのだった。cons
を使った場合
と比較してみよう。
(cons '(1 2 3 4) '(5 6 7 8)) |
こっちだと、cons
の最初の引数が出来たリストの最初の要素になって
しまう。
((1 2 3 4) 5 6 7 8) |
while
ループではなく再帰を使っても各々のファイルのリストに対して
作業することが出来る。再帰を使った lengths-list-many-files
は短く
て単純な形をしている。
再帰関数は、普通は `do-again-test'、`next-step-expression'、そして再帰呼
び出しの部分からなっている。`do-again-test' では、この関数が自分自身をも
う一度呼び出すかどうかを決定する。今の場合は list-of-files
がまだ
残りの要素を持っているかどうかを調べることになる。`next-step-expression'
では、list-of-files
をそれ自身の CDR で置き換える。結果とし
て最後にはリストは空になる。実際の完全なコードは、この説明よりも短い!
(defun recursive-lengths-list-many-files (list-of-files) "Return list of lengths of each defun in LIST-OF-FILES." (if list-of-files ; do-again-test (append (lengths-list-file (expand-file-name (car list-of-files))) (recursive-lengths-list-many-files (cdr list-of-files))))) |
一言で言うと、この関数は list-of-files
の最初のファイルについての
長さのリストを、list-of-files
の残りを引数に自分自身を呼び出した
結果に追加している。
実際に、各ファイルに対して lengths-list-file
を走らせながら
recursive-lengths-list-many-files
をテストしてみるには、次のよう
にする。
まず、まだやっていなければ recursive-lengths-list-many-files
と
lengths-list-files
をインストールし (訳註:
count-words-in-defun
もインストールする必要がある)、その後、次に
挙げるS式を評価する。ただし、ファイルのパス名は変更する必要があるかもし
れない。以下の式では、Info ファイルと Emacs のソースファイルが通常の位置
に置いてある場合に有効である。これを変更したい場合は、これらの
式を `*scratch*' バッファにコピーして、それらを編集した後、評価すれ
ばよい。
結果は `=>' の後に示されている。(これらの結果は Emacs version 18.57 についてのものである。他のバージョンの Emacs については、 また別の結果が出ることだろう。)
(lengths-list-file "../lisp/macros.el") => (176 154 86) (lengths-list-file "../lisp/mailalias.el") => (116 122 265) (lengths-list-file "../lisp/makesum.el") => (85 179) (recursive-lengths-list-many-files '("../lisp/macros.el" "../lisp/mailalias.el" "../lisp/makesum.el")) => (176 154 86 116 122 265 85 179) |
このように recursive-lengths-list-many-files
は期待した結果を返し
てくれるはずだ。
次のステップは、結果をグラフに表示するためのデータのリストを準備すること である。
recursive-lengths-list-many-files
関数は、数のリストを返す。各々
の数は関数定義の長さの記録である。我々がこれからやらねばならないのは、こ
のデータをグラフの表示に適した形の数値のデータに変換することである。新し
く出来るリストからは、10より少ない単語やシンボルしか含まない関数定義がど
れだけあるかとか、10から19や、20から29までではどうか等ということが分るよ
うになる。
手短に言うと、recursive-lengths-list-many-files
関数が生成したリ
ストを見ていって、各々の範囲に入る関数がどれだけあるかを数えて、それらの
数のリストを作ろうというのである。
これまでの経験から、長さのリストを `CDR' しつつ各々の値を見ていき、 それがどの範囲に入るのかを調べてその範囲についてのカウンタを増やす関数を 書くことは、特に難しくはないものと察しがつくだろう。
しかしながら、実際に関数を書き下す前に、長さのリストをまずソートして、少 ない方から大きい方に並べることによって得られるメリットについて考えるべき である。まず、ソートすることで、各々の範囲に属する関数の数を数えるのが楽 になる。これは隣同士の数は同じ範囲に属するか隣同士の範囲に属するかどちら かになるからである。また、リストをソートしてしまえば最大の数と最小の数を 簡単に見つけることが出来る。またそこから、後で必要となる最大と最小の差も 決定出来ることになる。
14.9.1 リストのソート 14.9.2 ファイルのリストの作成
Emacs は sort
と呼ばれるリストをソートするための関数を持っている。
sort
関数は二つの引数を持つ。ソートされるべきリストと、二つの要
素の大小を比較する際の述語 (predicate) である。
以前説明したように (関数に間違ったタイプ の引数を与えると, 参照)、述語とは、ある性質が真か偽かを判断する関数のこ
とである。sort
関数は、リストの要素を述語が使用する性質に従って並
べ換える。これは、数値以外のリストも、数値以外の基準で---例えばアルファ
ベットの順番で--- sort
を利用して並べ換えることが出来ることを示し
ている。
数値で比較する際には <
関数が使われる。例えば、
(sort '(4 8 21 17 33 7 21 7) '<) |
の結果は次のようになる。
(4 7 7 8 17 21 21 33) |
(この例では、引数が sort
に渡される際に評価されないように、どち
らのシンボルにも引用符が付いていることに注意しよう。)
recursive-lengths-list-many-files
関数によって返されたリストをソー
トするのは簡単である。
(sort (recursive-lengths-list-many-files '("../lisp/macros.el" "../lisp/mailalias.el" "../lisp/makesum.el")) '<) |
とするだけだ。結果は次のようになる。
(85 86 116 122 154 176 179 265) |
(この例では sort
の最初の引数には引用符がついていない。これは、
sort
に渡される前にこのS式を評価して、リストを生成する必要がある
からである。)
recursive-lengths-list-many-files
関数は引数としてファイルのリ
ストを必要とする。これまで実験した例では、これらのリストは手で作っていた。
しかし、Emacs Lisp のソースディレクトリは大変大きいので、これらを一々手
で書いているわけにはいかない。そこで、代わりに directory-files
関数
を作って、このようなリストを作成する必要がある。
directory-files
関数は、三つの引数を取る。最初の引数はディレクト
リの名前であり、文字列である。二番目の引数が non-nil
なら、この関
数はファイルの絶対パス名を返す。三番目の引数には nil
か正規表現を
指定する。正規表現を指定した場合、それにマッチするパス名を持つものだけが
返されることになる。
(訳註:Version 19 からは引数の数は四つになった。四番目の引数が
non-nil
なら結果は sort されない。)
従って、例えば私のシステムで
(length (directory-files "../lisp" t "\\.el$")) |
とやると、私の version 19.25 の Lisp のソースディレクトリには307の
`.el' ファイルがあることが分る。
recursive-lengths-list-many-files
が返すリストをソートするための
S式は次のようになる。
(sort (recursive-lengths-list-many-files (directory-files "../lisp" t "\\.el$")) '<) |
我々の取り敢えずの目標は、10未満の単語やシンボルしか含まない関数定義の数
はどれだけか、10以上、20未満ではどうか、20以上、30未満ではどうか、といっ
たことを調べることである。ソートされた数のリストを使うと、これは簡単であ
る。まずは、10未満の要素がどれだけあるかを数え、ついで、その次の要素から
20 未満の要素がどれだけか数え、また次の数から 30未満の要素がどれだけか数
える、というふうに続けていく。10、20、30、40等の数は、その範囲の数の最大
よりも大きい数になる。これらの数からなるリストは、top-of-ranges
リストと呼べばよいだろう。
しようと思えば、このようなリストを自動的に生成することも可能である。が、 今回は手で書いた方が早いだろう。次のような感じである。
(defvar top-of-ranges '(10 20 30 40 50 60 70 80 90 100 110 120 130 140 150 160 170 180 190 200 210 220 230 240 250 260 270 280 290 300) "List specifying ranges for `defuns-per-range'.") |
範囲を変更するには、このリストを編集すればよい。
次に、この各々の範囲に属する定義の数のリストを作る関数を書く必要がある。
明らかに、この関数は引数として sorted-lengths
と
top-of-ranges
リストを取ることになる。
defuns-per-range
関数は、二つの作業を何回も繰り返すことになる。一
つは現在の top-of-range の値によって特定される範囲の定義の数を数えること、
もう一つはその範囲の数を数え終わったら次に大きな top-of-ranges
の
値に移ることである。これらの動作は繰り返しなので、while
ループを
使うことが出来る。片方のループで現在の top-of-range の値で決まる範囲の定
義の数をカウントし、もう片方のループでは順に top-of-range の値を選択して
いく。
各々の範囲について、sorted-lengths
リストの中の幾つかのエントリ
がカウントされる。従って、sorted-lengths
リストについてのループは
top-of-ranges
リストのループの中に置かれることになる。大きなギヤの
なかの小さなギヤみたいな感じだ。
内部のループでは、該当する範囲の定義の数がカウントされる。これは、今まで
に何回も見たような単純なループである。
(増加するカウンタを使ったループ, を参照。) ルー
プの真偽テストは sorted-lengths
リストの中の数が現在の
top-of-range の値よりも小さいかどうかを見ることになる。もしそうなら、カ
ウンタを一つ増やして、次の sorted-lengths
のエントリに移動する。
結局、内部のループは次のようになる。
(while 長さの要素が top-of-range より小さい (setq number-within-range (1+ number-within-range)) (setq sorted-lengths (cdr sorted-lengths))) |
外部のループは top-of-ranges
リストの最小値から始まって、順に大き
な値に移っていくことになる。そのためには、次のようにすればよい。
(while top-of-ranges ループの本体... (setq top-of-ranges (cdr top-of-ranges))) |
これらを合わせると、二つのループは次のようになる。
(while top-of-ranges ;; 現在の範囲にある要素の数のカウント (while 長さの要素が top-of-range より小さい (setq number-within-range (1+ number-within-range)) (setq sorted-lengths (cdr sorted-lengths))) ;; 次の範囲に移動 (setq top-of-ranges (cdr top-of-ranges))) |
更に、一回の外部ループごとに、Emacs にその範囲に属する定義の数を記録させ
る必要がある。(リストの中の number-within-range
の値である。この
目的には、cons
が使える。(cons
, を参照。)
cons
関数はほぼうまく動作するのだが、一つ難点がある。出来るリスト
では最初に大きい方の範囲に入る定義の数がきて、最後に小さい方の範囲の数が
きてしまうのだ。これは、cons
が新しい要素をリストの先頭に加えてい
くことと、上の二つのループは小さい方から大きい方へ長さのリストを作成して
いくために、defuns-per-range-list
が最大の数で終わることからの当
然の帰結である。しかし、グラフを表示する際には小さい値の方を先に表示した
い。この問題を解決するには、defuns-per-range-list
の順序を逆にし
てしまえばよい。これは、nreverse
というリストの順序を逆にする関数
を使うとあっさり解決する。
例えば、
(nreverse '(1 2 3 4)) |
とすると、
(4 3 2 1) |
が返る。
注意して欲しいのは、nreverse
は「破壊的」であるということである。
これは、作用させたリストを変更してしまうことを意味している。(訳註:これ
は逆の順のリストに設定されるということではなくて、文字通り破壊されてしま
うということである。) 今の場合、元の defuns-per-range-list
は必要
ないので、これが破壊されても何の問題もない。(一方、reverse
関数は
元のリストを逆に並べ換えた新しいリストを返す。この場合は元のリストは変化
しない。)
以上を全て組み合わせると、defuns-per-range
は次のようになる。
(defun defuns-per-range (sorted-lengths top-of-ranges) "SORTED-LENGTHS defuns in each TOP-OF-RANGES range." (let ((top-of-range (car top-of-ranges)) (number-within-range 0) defuns-per-range-list) ;; 外部のループ (while top-of-ranges ;; 内部のループ (while (and ;; 数値引数として数が必要 (car sorted-lengths) (< (car sorted-lengths) top-of-range)) ;; 現在の範囲に入る関数の数を数える (setq number-within-range (1+ number-within-range)) (setq sorted-lengths (cdr sorted-lengths))) ;; 内部のループは抜けるが、外部のループには入ったまま (setq defuns-per-range-list (cons number-within-range defuns-per-range-list)) (setq number-within-range 0) ; カウンタを 0 にリセット ;; 次の範囲に移動 (setq top-of-ranges (cdr top-of-ranges)) ;; 次の範囲のトップを特定 (setq top-of-range (car top-of-ranges))) ;; 外部のループを抜けて最も大きいの範囲に属する関数定義の数 ;; を数える (setq defuns-per-range-list (cons (length sorted-lengths) defuns-per-range-list)) ;; 昇順で並ぶ関数定義の長さのリストを返す。 (nreverse defuns-per-range-list))) |
この関数は次のちょっとした点を除いては、非常に単純である。内部のループの真 偽テストは、
(and (car sorted-lengths) (< (car sorted-lengths) top-of-range)) |
であって、
(< (car sorted-lengths) top-of-range) |
ではない。このテストの目的は sorted-lengths
リストの最初の要素が
その時点での top-of-range の値よりも小さいかどうかを決定することである。
後に挙げた単純な方のテストでも sorted-lengths
リストが nil
になるまではうまく動作する。しかし、nil
になると、(car
sorted-lengths)
式は nil
を返す。<
関数は数値と
nil
、即ち空リストとを比較できないため、Emacs はここでエラーを出し、
関数はそこで実行を止めてしまう。
sorted-lengths
リストはカウンタがリストの最後まで辿りつけば常に
nil
になる。従って、この defuns-per-range
関数の真偽テスト
の単純なバージョンの方は常に失敗することになる。
この問題は、(car sorted-lengths)
式と and
式を組み合わせ
ることで解決することが出来る。(car sorted-lengths)
式はリストが最
低一つでも要素を持てば、non-nil
を返す。そしてリストが空になった
時だけ nil
を返す。and
式は最初に (car
sorted-lengths)
式を評価し、もしそれが nil
なら <
式を評
価する前に偽を返す。しかし、もし (car sorted-lengths)
式が
non-nil
な値を返せば、<
式も評価し、その値を and
式
全体の値を返す。
こうしてエラーが回避出来ることになる。
and
についての詳細は、12.4 forward-paragraph
:関数の金脈, を参照のこと。
次に defuns-per-range
についての簡単なテストを載せておく。最初に
(短縮した) リストを top-of-ranges
にバインドするS式を評価し、次
に sorted-lengths
リストをバインドするS式を評価し、最後に
defuns-per-range
関数を評価してみよう。
;; (後で使うものよりかは短いリスト) (setq top-of-ranges '(110 120 130 140 150 160 170 180 190 200)) (setq sorted-lengths '(85 86 110 116 122 129 154 176 179 200 265 300 300)) (defuns-per-range sorted-lengths top-of-ranges) |
次のようなリストが返されるはずである。
(2 2 2 0 0 1 0 2 0 0 4) |
実際、sorted-lengths
リストには、110未満の二つの要素が二つ、110か
ら119までの要素も二つ、120から129までも二つ、といった感じになっている。
また、200以上の値の要素は四つある。
[ << ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |