[ << ] [ >> ]           [Top] [Contents] [Index] [ ? ]

14. defun 内の単語のカウント

次の計画の目標は、関数定義の中の単語の数を数えることである。当たり前のこ とだが、これは count-words-region の使い方をちょっと工夫すれば出 来てしまう。カウント:繰り返しと正規表現, を参照 のこと。例えば、ある一つの定義の中の単語数を数えたければ、C-M-h (mark-defun) コマンドを使って定義部分をマークしてから count-words-region を呼び出せばよい。

しかしながら、ここではもっと大きなことをやってみたいと思う。Emacs のソー スの中の全ての定義の中の単語とシンボルの数を数えて、そこにどれだけの関数 があり、各々がどのくらいの長さかをグラフにして出力するとか、40個から49個 までの単語とシンボルをもつ関数がどれだけあるか、50個から59個までではどうか、 といったことを調べるのである。私はしばしば典型的な関数というのがどのくらい の長さかを知りたくなる。これは、そういったことを教えてくれるものである。

分割による困難の克服  
14.1 何を数えればよいか?  
14.2 単語やシンボルは何から構成されているか  
14.3 関数 count-words-in-defun  count-words とほぼ同じ
14.4 一つのファイルにある複数の defun を数える  
14.5 ファイルを見つける  
14.6 lengths-list-file についての詳細  沢山の定義の長さのリスト
14.7 異なるファイルの中の定義を数える  
14.8 異なるファイルの定義を再帰を使って数える  
14.9 データをグラフに表示するための準備  

分割による困難の克服

はっきり言って、このヒストグラムを書く計画は人をひるませる類のものである。 しかし、これをいくつもの細かいステップに分けて、各々を一つずつ見ていくこ とにすれば、それほど恐れるほどのものではない。そこで、どんなステップに分 けるべきかを書いてみることにする。

これはかなりの大計画である。しかし、各々のステップをゆっくりと進んでいけ ば、それ程困難なものではない。


14.1 何を数えればよいか?

関数定義の中の単語数を数えるにはどうしたらよいか、を最初に考え始めた時に、 まず疑問に思うこと (あるいは、考えるべきこと) は、我々は何を数えればよい かということである。Lisp の関数定義に関して単語のことを話す場合、実際に は大抵シンボルのことを言っている。例えば、次の multiply-by-seven 関数は、defunmultiply-by-sevennumber*、 そして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) の問題になる。これには一つのセクションを割 当てて論じる価値がある。


14.2 単語やシンボルは何から構成されているか

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]*"


14.3 関数 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個の単語とシンボルがある。

次の問題は、一つのファイルの中の幾つかの定義にある単語とシンボルの数を 数えることである。


14.4 一つのファイルにある複数の defun を数える

`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) ことは、この文書では初めて出てくるプロセスであ る。


14.5 ファイルを見つける

まずは、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-noselectset-buffer を使えばよいのである。


14.6 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)

ファイルの中の最後の定義の長さは、リストの最初に現れることに注意しよう。


14.7 異なるファイルの中の定義を数える

前節では、各ファイルの中に含まれる各関数の長さのリストを返すような関数を 作成したのだった。今度は、ファイルのリストが与えられた時に、それらのファ イルの中の関数の長さのマスターリストを返すような関数を定義してみたい。

リストの中の各ファイルに対する作業は繰り返しの動作なので、while ループや再帰を使って行うことが出来る。

while ループを使った方法はルーティーンワークである。関数に渡す引 き数はファイルのリストになる。以前見たように (ループの例, 参照)、 このリストが要素を含んでいる時のみループの本体を実行し、要素が無くなった ら抜けるようにすることが出来るのであった。これがうまく動作するためには、 本体部分で、本体が一回評価されるごとにこのリストを短くしていき、結果とし て最後にはリストが空になるように、S式を書いておく必要がある。このために は、本体が評価されるごとに、リストにそのリストの CDR の値をセットす るという技法を用いるのが普通である。

テンプレートは次のようになる。

 
(while リストが空かどうかのテスト
  本体...
  リストを自分自身の cdr にセット)

さて、while ループは常に (真偽テストの結果として) nil を返 し、本体内のS式の値を返したりすることはないのだった。(従って、ループの 本体のS式は副作用として評価される。) しかしながら、長さのリストをセッ トするS式は本体の一部である---にもかかわらず、その関数全体の値として返 して欲しいのもこの値である。そこで、while ループを let 式 で包んで、let 式の最後の要素が長さのリストの値を含むようにする。 (増加するカウンタを使ったループの例, を参 照。)

以上のことを考えれば、目的の関数が殆ど書けてしまう。

 
;;; while ループ を使う。
(defun lengths-list-many-files (list-of-files) 
  "Return list of lengths of defuns in LIST-OF-FILES."
  (let (lengths-list)

;;; 真偽テスト
    (while list-of-files        
      (setq lengths-list
            (append
             lengths-list

;;; 長さのリストの生成
             (lengths-list-file
              (expand-file-name (car list-of-files)))))

;;; ファイルのリストを短くする。
      (setq list-of-files (cdr list-of-files))) 

;;; リストの長さの最終的な値を返す。
    lengths-list))              

expand-file-name は組み込み関数であり、ファイル名を絶対パスも含 めた省略無しの形に戻すものである。従って、例えば

 
debug.el

 
/usr/local/emacs/lisp/debug.el

と展開される。

その他の、上の関数内に出てくる新しい要素は append だけである。こ れには一つのセクションを割当てた方が良いだろう。

14.7.1 関数 append  あるリストを別のリストに追加する


14.7.1 関数 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)


14.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-fileslengths-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 は期待した結果を返し てくれるはずだ。

次のステップは、結果をグラフに表示するためのデータのリストを準備すること である。


14.9 データをグラフに表示するための準備

recursive-lengths-list-many-files 関数は、数のリストを返す。各々 の数は関数定義の長さの記録である。我々がこれからやらねばならないのは、こ のデータをグラフの表示に適した形の数値のデータに変換することである。新し く出来るリストからは、10より少ない単語やシンボルしか含まない関数定義がど れだけあるかとか、10から19や、20から29までではどうか等ということが分るよ うになる。

手短に言うと、recursive-lengths-list-many-files 関数が生成したリ ストを見ていって、各々の範囲に入る関数がどれだけあるかを数えて、それらの 数のリストを作ろうというのである。

これまでの経験から、長さのリストを `CDR' しつつ各々の値を見ていき、 それがどの範囲に入るのかを調べてその範囲についてのカウンタを増やす関数を 書くことは、特に難しくはないものと察しがつくだろう。

しかしながら、実際に関数を書き下す前に、長さのリストをまずソートして、少 ない方から大きい方に並べることによって得られるメリットについて考えるべき である。まず、ソートすることで、各々の範囲に属する関数の数を数えるのが楽 になる。これは隣同士の数は同じ範囲に属するか隣同士の範囲に属するかどちら かになるからである。また、リストをソートしてしまえば最大の数と最小の数を 簡単に見つけることが出来る。またそこから、後で必要となる最大と最小の差も 決定出来ることになる。

14.9.1 リストのソート  
14.9.2 ファイルのリストの作成  


14.9.1 リストのソート

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式を評価して、リストを生成する必要がある からである。)


14.9.2 ファイルのリストの作成

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-lengthstop-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] [ ? ]

This document was generated by Matsuda Shigeki on April, 10 2002 using texi2html