defun内の単語の数え上げ
[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [表紙] [目次] [索引] [検索] [上端 / 下端] [?]

14. defun内の単語の数え上げ

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=eintro&node=Words%20in%20a%20defun"
"intro/defun内の単語の数え上げ"へのコメント(無し)

つぎの目標は、関数定義内の語数を数えることである。 明らかに、count-words-regionの変形を使えばできる。 See 節 13. 数え上げ:繰り返しと正規表現。 1つの定義内の単語を数えるだけならば、 定義をコマンドC-M-hmark-defun)でマークして、 count-words-regionを呼べばよい。

しかし、より野心的でありたい。 Emacsソース内の各定義ごとに単語やシンボルを数え、それぞれの数ごとに いくつの関数があるかを表すグラフを書きたい。 40から49個の単語やシンボルを含む関数はいくつ、 50から59個の単語やシンボルを含む関数はいくつなどを表示したいのである。 筆者は、典型的な関数定義の長さに興味を持っており、この関数でそれがわかる。

Divide and Conquer

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=eintro&node=Divide%20and%20Conquer"
"intro/DivideandConquer"へのコメント(無し)

一言でいえば、目標はヒストグラムを作ることである。 多数の小さな部分に分けて一度に1つずつ扱えば、恐れることはない。 どのような手順を踏めばよいか考えよう。

これはとても手応えのあることである。 しかし、順を追って進めれば、困難ではない。



[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [表紙] [目次] [索引] [検索] [上端 / 下端] [?]

14.1 何を数えるか?

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=eintro&node=Words%20and%20Symbols"
"intro/何を数えるか?"へのコメント(無し)

関数定義内の単語を数えることを考えた場合、 最初の疑問は何を数えるのかということである。 Lispの関数定義に関して「単語」といえば、ほとんどの場合、 「シンボル」のことである。 たとえば、つぎの関数multiply-by-sevenには、 defunmultiply-by-sevennumber*7の5つのシンボルがある。 さらに、4つの単語、`Multiply'、`NUMBER'、`by'、`seven'を 含んだ説明文字列がある。 シンボル`number'は繰り返し使われているので、 定義には全部で10個の単語とシンボルが含まれる。

 
(defun multiply-by-seven (number)
  "Multiply NUMBER by seven."
  (* 7 number))

しかし、multiply-by-sevenの定義にC-M-hmark-defun)で マークしてからcount-words-regionを呼ぶと、 count-words-regionは定義には10ではなく11語があると報告する!  何かがおかしい!

問題は2つあり、count-words-regionは`*'を単語として数えないことと、 単一のシンボルmultiply-by-sevenを3語として数えることである。 ハイフンを単語を繋ぐ文字としてではなく、単語のあいだの空白として扱う。 `multiply-by-seven'は、 `multiply by seven'と書かれているかのように数えられる。

このような混乱の原因は、count-words-regionの定義のなかの ポイントを単語単位に進める正規表現の探索にある。 count-words-regionの正規表現はつぎのとおりである。

 
"\\w+\\W*"

この正規表現は、単語の構成文字の1個以上の繰り返しに 単語を構成しない文字が0個以上繰り返したものを続けたパターンである。 つまり、「単語の構成文字」ということでシンタックスの問題になるのであり、 その重要性から1つの節で扱おう。



[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [表紙] [目次] [索引] [検索] [上端 / 下端] [?]

14.2 単語やシンボルを構成するものは何か?

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=eintro&node=Syntax"
"intro/単語やシンボルを構成するものは何か?"へのコメント(無し)

Emacsは、それぞれの文字をそれぞれが属する シンタックスカテゴリ(syntax categories)に応じて扱う。 たとえば、正規表現`\\w+'は、単語構成文字の1個以上の繰り返しを 意味するパターンである。 単語構成文字は、1つのシンタックスカテゴリである。 他のシンタックスカテゴリには、ピリオドやカンマなどの句読点文字のクラスや、 空白文字やタブ文字などの空白文字クラスがある (より詳しくは、節 `The Syntax Table' in

GNU Emacsマニュアル
や節 `Syntax Tables' in
GNU Emacs Lispリファレンスマニュアル
を参照)。

シンタックステーブルは、どの文字がどのカテゴリに属するかを指定する。 普通、ハイフンは「単語構成文字」ではない。 かわりに、「シンボルの一部ではあるが単語の一部ではないクラス」に指定される。 つまり、関数count-words-regionは、ハイフンを単語のあいだの 空白と同様に扱い、そのためcount-words-regionは `multiply-by-seven'を3語と数えるのである。

Emacsに`multiply-by-seven'を1つのシンボルとして数えさせるには、 2つの方法がある。 シンタックステーブルを変更するか、正規表現を変更するかである。

Emacsが各モードごとに保持するシンタックステーブルを変更して、 ハイフンを単語構成文字として再定義することもできる。 この方法でもわれわれの目的は達せられるが、 ハイフンはシンボルの一部になるもっとも一般的な文字であるが、 典型的な単語構成文字ではない。 このような文字は他にもある。

かわりに、count-words-regionの定義の中の正規表現を シンボルを含むように再定義することである。 この方法は明確ではあるが、技巧を要する。

最初の部分は簡単で、パターンは「単語やシンボルを構成する少なくとも1文字」 である。 つまり、つぎのようになる。

 
\\(\\w\\|\\s_\\)+

`\\('は、`\\|'で区切られた`\\w'と代替の`\\s_'を 含むグループ構成の開始部分である。 `\\w'は任意の単語構成文字に一致し、 `\\s_'は単語構成文字ではないがシンボル名の一部に 成りえる任意の文字に一致する。 グループに続く`+'は、単語やシンボルを構成する文字が少なくとも1つ あることを意味する。

しかし、正規表現の第二の部分は、設計がより困難である。 最初の部分に続けて、「単語やシンボルを構成しない文字が0個以上続く」と 指定したいのである。 まず、筆者は、つぎのような定義を考えた。

 
\\(\\W\\|\\S_\\)*"

大文字の`W'や`S'は、単語やシンボルを構成しない文字に一致する。 残念ながら、この式は、単語を構成しない任意の文字かシンボルを構成しない 任意の文字に一致する。 つまり、任意の文字に一致するのである!

筆者が試したリージョンでは、単語やシンボルには空白文字 (空白、タブ、改行)が続いていることに気づいた。 そこで、単語やシンボルを構成する文字の1個以上の繰り返しパターンのあとに 1個以上の空白文字が続くパターンを試してみた。 これも失敗であった。 単語やシンボルはしばしば空白で区切られるが、実際のコードでは、 シンボルのあとには括弧が、単語のあとには句読点が続く。 結局、単語やシンボルを構成する文字に続いて空白文字以外の文字が0個以上続き、 さらに、空白文字が0個以上続くパターンにした。

全体の正規表現はつぎのとおりである。

 
"\\(\\w\\|\\s_\\)+[^ \t\n]*[ \t\n]*"



[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [表紙] [目次] [索引] [検索] [上端 / 下端] [?]

14.3 関数count-words-in-defun

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=eintro&node=count-words-in-defun"
"intro/関数count-words-in-defun"へのコメント(無し)

関数count-words-regionの書き方には、何通りかあることを説明した。 count-words-in-defunを書くには、それらの1つを適用すればよい。

whileループを使った版は、容易に理解できるので、 これを適用することにする。 count-words-in-defunは、複雑なプログラムの一部となるので、 対話的である必要はなく、メッセージを表示する必要もなく、 単に語数を返せばよい。 このため、定義は少々簡単になる。

一方、count-words-in-defunは、関数定義を含んだバッファで利用される。 つまり、関数定義内にポイントがある状態で呼ばれたかどうかを調べ、 もしそうなら、その定義の語数を返すようにするのが合理的であろう。 こうすると定義に余分な複雑さを伴うが、関数に引数を渡さないですむ。

以上から、関数の雛型はつぎのようになる。

 
(defun count-words-in-defun ()
  "説明文..."
  (初期設定...
     (whileループ...)
   語数を返す)

いつものように、各項目を埋めていく。

まずは、初期設定である。

この関数は、関数定義を含むバッファで呼ばれると仮定している。 ポイントは、関数定義の内側か外側にある。 count-words-in-defunが動作するには、ポイントを定義の先頭に移動し、 カウンタを0で始め、ポイントが定義の最後に達したらループを終了する。

関数beginning-of-defunは、行の先頭にある`('などの 開き区切り記号を逆向きに探し、ポイントをそこへ移動する。 あるいは、探索範囲内で止まる。 実際、beginning-of-defunは、ポイントを囲んでいる関数定義の先頭か 直前の関数定義の先頭にポイントを移動するか、バッファの先頭に移動する。 ポイントを開始場所へ移動するにはbeginning-of-defunを使えばよい。

whileループには、単語やシンボルを数えるカウンタが必要である。 let式で、このためのローカル変数を作って、初期値0に束縛する。

関数end-of-defunは、beginning-of-defunと同じように動作するが、 関数定義の最後にポイントを移動する点が異なる。 end-of-defunは、関数定義の最後に達したかどうかを調べる式に使える。

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の初期設定後の2番目の部分は、 whileループである。

ループには、語単位やシンボル単位でポインタを進める式や、 語数を数える式が必要である。 whileの判定条件は、ポイントを進められた場合には真を、 ポイントが定義の最後に達した場合には偽を返す必要がある。 これ(see 節 14.2 単語やシンボルを構成するものは何か?) に必要な正規表現はすでにわかっているので、ループは簡単である。

 
(while (and (< (point) end)
            (re-search-forward 
             "\\(\\w\\|\\s_\\)+[^ \t\n]*[ \t\n]*" end t)
  (setq count (1+ count)))

関数定義の3番目の部分では、単語やシンボルの個数を返す。 この部分は、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))

これをどのようにテストしようか?  関数は対話的ではないが、対話的に呼び出すための呼び出し関数を 作るのは簡単である。 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-defuncount-words-defunをインストールし、 キーバインドを設定してから、つぎの関数定義にカーソルを置く。

 
(defun multiply-by-seven (number)
  "Multiply NUMBER by seven."
  (* 7 number))
     => 10

うまくいった!  定義には10個の単語やシンボルがある。

つぎの問題は、1つのファイルの中にある複数の定義の中の単語やシンボルの 個数を数えることである。



[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [表紙] [目次] [索引] [検索] [上端 / 下端] [?]

14.4 ファイル内の複数のdefunsの数え上げ

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=eintro&node=14.4%20ファイル内の複数のdefunsの数え上げ"
"intro/ファイル内の複数のdefunsの数え上げ"へのコメント(無し)

`simple.el'のようなファイルには80以上もの関数定義が含まれる。 われわれの最終目標は、複数のファイルについて統計を集めることであるが、 第1段階としての中間目標は、1つのファイルについて統計を集めることである。

この情報は、関数定義の長さを表す数を並べたものになる。 これらの数は、リストに収めることができる。

1つのファイルから得た情報を他の多くのファイルから得た情報に 加える必要があるので、この関数では、1つのファイル内の定義を 数えた数をリストにして返す必要がある。 この関数は、いかなるメッセージも表示する必要はなく、また、してはならない。

語数を数えるコマンドには、語単位にポイントを進める式と、 語数を数える式が含まれる。 関数定義の長さを数える関数も同じように、 定義単位にポイントを進める式と長さのリストを作る式で 動作するように設計できる。

問題をこのように文書にすることは、関数定義を書く基本である。 ファイルの先頭から数え始めるので、最初のコマンドは (goto-char (point-min))になる。 つぎに、whileループを始めるのであるが、 ループの判定条件は、つぎの関数定義を探す正規表現の探索式である。 探索に成功する限りポイントを先に進め、ループの本体を評価する。 本体には、長さのリストを作る式が必要である。 リスト作成のコマンドconsを使ってリストを作る。 これでほとんどできあがりである。

コードの断片を示そう。

 
(goto-char (point-min))
(while (re-search-forward "^(defun" nil t)
  (setq lengths-list
        (cons (count-words-in-defun) lengths-list)))

残っているのは、関数定義を収めたファイルを探す機構である。

まえの例では、Infoファイルやバッファ`*scratch*'などの他のバッファに 切り替えていた。

ファイルを探すという動作は、まだ説明していない新しい動作である。



[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [表紙] [目次] [索引] [検索] [上端 / 下端] [?]

14.5 ファイルを探す

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=eintro&node=Find%20a%20File"
"intro/ファイルを探す"へのコメント(無し)

Emacsでファイルを探すには、コマンドC-x C-ffind-file)を使う。 このコマンドは、われわれの長さの問題にはほぼよいのではあるが、 ぴったりではない。

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

定義には短いが完全な説明文があり、 コマンドを対話的に使った場合にファイル名を 問い合わせるinteractive式もある。 定義の本体には、2つの関数、find-file-noselectswitch-to-bufferがある。

C-h f(コマンドdescribe-function)で表示される説明文によれば、 関数find-file-noselectは、指定されたファイルをバッファに読み込み、 そのバッファを返す。 しかし、バッファを選択することはしない。 (find-file-noselectを使った場合、) Emacsは指定したバッファに注意を向けないのである。 これはswitch-to-bufferが行うことであり、 Emacsの注意を指定したバッファに向け、そのバッファをウィンドウに表示する。 バッファの切り替えについてはすでに説明した (See 節 2.3 バッファの切り替え)。

われわれのヒストグラムの計画では、 プログラムが各定義の長さを調べる個々のファイルを表示する必要はない。 switch-to-bufferを使うかわりに、 set-bufferを使って、プログラムの注意を別のバッファに向けるが、 画面には表示しない。 したがって、find-fileを呼んで操作するかわりに、 独自の式を書く必要がある。

これは簡単であり、find-file-noselectset-bufferを使う。



[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [表紙] [目次] [索引] [検索] [上端 / 下端] [?]

14.6 lengths-list-fileの詳細

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=eintro&node=lengths-list-file"
"intro/lengths-list-fileの詳細"へのコメント(無し)

関数lengths-list-fileの核は、defun単位にポイントを移動する機能と、 各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)))

関数には1つの引数、処理すべきファイル名を渡す。 4行の説明文があるが、interactive式はない。 何も表示されないとコンピュータが壊れたと心配する人がいるので、 本体の最初の行でメッセージを表示する。

つぎの行には、save-excursionがあり、関数が終了したときに Emacsの注意をカレントバッファに戻す。 これは、もとのバッファにポイントが戻ると仮定している関数から この関数を使う場合に便利である。

let式の変数リストでは、Emacsが探したファイルを含んだバッファに ローカル変数bufferを束縛する。 同時に、Emacsはローカル変数としてlengths-listを作る。

続いて、Emacsはバッファに注意を向ける。

続く行では、Emacsはバッファを読み出し専用にする。 理想的にはこの行は必要ない。 関数定義内の単語やシンボルを数える関数は、バッファを変更しない。 さらに、バッファを変更したとしてもバッファを保存しない。 この行は、最大限の注意を払ったものである。 注意する理由は、この関数やこれが呼び出す関数はEmacsのソースを処理するので、 それらが不注意に変更してしまうと、とても都合が悪い。 テストに失敗してEmacsのソースファイルを変更してしまうまでは、 筆者もこの行の必要性を認識していなかった。

つぎに続くのは、バッファがナロイングされている場合に備えたワイドニングである。 この関数も普通は必要ない。 バッファが既存でなければEmacsは新たにバッファを作成する。 しかし、ファイルを訪問しているバッファが既存ならば、 Emacsはそのバッファを返す。 この場合には、バッファがナロイングされている可能性があるので ワイドニングする。 完全に「ユーザーフレンドリ」を目指すならば、ナロイングやポイントの位置を 保存するべきであるが、ここではしていない。

(goto-char (point-min))は、ポイントをバッファの先頭へ移動する。

つぎに続くのは、この関数の処理を行うwhileループである。 このループでは、Emacsは、各定義の長さを調べ、情報を収めた長さのリストを作る。

処理を終えると、Emacsはバッファをキルする。 これは、Emacsが使用するメモリを節約するためである。 筆者のEmacs第19版には、調べたいファイルが300以上もある。 別の関数で、各ファイルにlengths-list-fileを適用する。 Emacsがすべてのソースファイルを訪問して1つも削除しないと、 筆者のコンピュータの仮想メモリを使い切ってしまう。

let式の最後の式は、変数lengths-listであり、 関数全体の値として返される値である。

いつものようにこの関数をインストールすれば、試せる。 つぎの式の直後にカーソルを置いてC-x C-eeval-last-sexp)と タイプする。

 
(lengths-list-file "../lisp/debug.el")

(ファイルのパス名を変更する必要があろう。 InfoファイルとEmacsのソースが /usr/local/emacs/info/usr/local/emacs/lispのように 隣り合っていれば、このパスでよい。 式を変更するには、バッファ`*scratch*'にこの式をコピーして、 それを修正する。 そして、それを評価する。)

筆者のEmacsの版では、`debug.el'の長さのリストを調べるのに 7秒かかり、つぎのような結果を得た。

 
(  34 235)

ファイルの最後にある定義の長さがリストの先頭にあることに注意してほしい。



[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [表紙] [目次] [索引] [検索] [上端 / 下端] [?]

14.7 別のファイルのdefuns内の単語の数え上げ

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=eintro&node=Several%20files"
"intro/別のファイルのdefuns内の単語の数え上げ"へのコメント(無し)

前節では、ファイル内の各定義の長さをリストにして返す関数を作成した。 ここでは、複数のファイルの各定義の長さのリストを返す関数を定義する。

ファイルの並びのおのおのを処理することは繰り返し動作であるので、 whileループか再帰を使える。

whileループを使った設計は、決まりきっている。 関数に渡す引数はファイルのリストである。 すでに見てきたように(see 節 11.1.1 whileループとリスト)、 リストに要素がある限りループの本体を評価し、 リストが空になったらループを抜け出るように whileループを書くことができる。 これが動作するためには、本体を評価するたびにリストを短くし、 最終的にはリストが空になるような式がループの本体に含まれる必要がある。

雛型はつぎのようになる。

 
(while リストが空かどうか調べる
  本体...
  リストにリストのcdrを設定)

whileループは(判定条件を評価した結果である)nilを返し、 本体の評価結果を返さない (ループの本体内の式は、副作用のために評価される)。 しかし、長さのリストを設定する式は本体の中にあり、 その値を関数全体の値としてほしいのである。 これには、whileループをlet式で囲み、 let式の最後の要素が長さのリストの値を含むようにする (See 節 増加カウンタの例)。

以上のことを考慮すると、関数はつぎのようになる。

 
;;; 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    Attaching one list to another.



[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [表紙] [目次] [索引] [検索] [上端 / 下端] [?]

14.7.1 関数append

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=eintro&node=append"
"intro/関数append"へのコメント(無し)

関数appendは、リストを他のリストに繋げる。 たとえば、

 
(append '(1 2 3 4) '(5 6 7 8))

は、つぎのリストを作り出す。

 
(1 2 3 4 5 6 7 8)

lengths-list-fileを呼び出して得た2つのリストを、 このように繋ぎたい。 結果は、consと対照的である。

 
(cons '(1 2 3 4) '(5 6 7 8))

では、consの第1引数が新たなリストの第1要素になる。

 
((1 2 3 4) 5 6 7 8)



[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [表紙] [目次] [索引] [検索] [上端 / 下端] [?]

14.8 別のファイルの再帰による単語の数え上げ

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=eintro&node=Several%20files%20recursively"
"intro/別のファイルの再帰による単語の数え上げ"へのコメント(無し)

whileループのかわりに、 ファイルのリストのおのおのを再帰的に処理してもできる。 lengths-list-many-filesの再帰版は、短くて簡単である。

再帰的関数には、「再帰条件」、「次段式」、再帰呼び出しの部分がある。 「再帰条件」は、関数が再度自身を呼び出すかどうかを決めるもので、 list-of-filesに要素があれば自身を呼び出す。 「次段式」は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                     ; 再帰条件
      (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-fileを インストールしてから、つぎの式を評価する。 ファイルのパス名は変更する必要があるだろう。 InfoファイルとEmacsのソースがデフォルトの場所にあれば、 変更する必要はない。 式を変更するには、これらをバッファ`*scratch*'にコピーして、 そこで変更して評価する。

結果は`=>'のあとに記した (これらの値はEmacs第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は、望みの結果を出している。

つぎの段階は、グラフ表示するためにリスト内のデータを準備することである。



[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [表紙] [目次] [索引] [検索] [上端 / 下端] [?]

14.9 グラフ表示用データの準備

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=eintro&node=Prepare%20the%20data"
"intro/グラフ表示用データの準備"へのコメント(無し)

関数recursive-lengths-list-many-filesは、個数のリストを返す。 各個数は、関数定義の長さである。 このデータを、グラフ作成に適した数のリストに変換したいのである。 新しいリストでは、単語やシンボルが10個未満の関数定義はいくつ、 10から19個のものはいくつ、20から29個のものはいくつ、などとしたい。

つまり、関数recursive-lengths-list-many-filesが作成した 長さのリスト全体を処理して、長さの範囲ごとに数を数えて、 これらの数から成るリストを作りたいのである。

これまでの知識をもとにすれば、 長さのリストの「CDRs」を辿りながら、各要素を調べ、 どの長さの範囲に含まれるかを決めて、その範囲のカウンタを増やす関数を 書くことは難しくないことがわかる。

しかし、そのような関数を書き始めるまえに、 長さのリストを最小数から最大数の順にソートした場合の利点を考えてみよう。 まず、ソートしてあると、2つの並んだ数は同じ範囲か隣り合う範囲にあるので、 各範囲の数を数えるのが簡単になる。 第二に、ソートされたリストを調べれば、最大数と最小数がわかるので、 最大の範囲と最小の範囲を決定できる。

14.9.1 リストのソート(整列)    Sorting lists.
14.9.2 ファイルのリストの作成    Making a list of files.



[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [表紙] [目次] [索引] [検索] [上端 / 下端] [?]

14.9.1 リストのソート(整列)

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=eintro&node=Sorting"
"intro/リストのソート(整列)"へのコメント(無し)

Emacsにはリストをソートする関数sortがある。 関数sortは2つの引数、ソートすべきリストと、 リストの2つの要素のうち最初のものが2番目より「小さい」かどうかを 調べる述語を取る。

すでに説明したように (see 節 1.8.4 引数に誤った型のオブジェクトを指定) 述語とは、ある性質が真か偽かを調べる関数である。 関数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  265)

(この例では、sortに渡すリストを生成するために 式を評価する必要があるので、sortの第1引数はクオートしない。)



[ < ] [ > ]   [ << ] [ Up ] [ >> ]         [表紙] [目次] [索引] [検索] [上端 / 下端] [?]

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

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=eintro&node=Files%20List"
"intro/ファイルのリストの作成"へのコメント(無し)

関数recursive-lengths-list-many-filesは、 引数としてファイルのリストを必要とする。 上の例では、リストを手で書いた。 しかし、Emacs Lispのソースディレクトリは、こうするには大きすぎる。 かわりに、リストを作るために関数directory-filesを使う必要がある。

関数directory-filesは、3つの引数を取る。 第1引数は、ディレクトリ名を表す文字列である。 第2引数にnil以外の値を指定すると、 関数はファイルの絶対パス名を返す。 第3引数は選択子である。 これに(nilではなく)正規表現を指定すると、 この正規表現に一致するパス名のみを返す。

したがって、筆者のシステムで、

 
(length
 (directory-files "../lisp" t "\\.el$"))

とすると、筆者の第19.25版のLispのソースディレクトリには307個の `.el'ファイルがあることがわかる。

recursive-lengths-list-many-filesが返したリストをソートする式は つぎのようになる。

 
(sort
 (recursive-lengths-list-many-files
  (directory-files "../lisp" t "\\.el$"))
 '<)

われわれの中間目標は、単語やシンボルが10個未満の関数定義はいくつ、 10から19個のものはいくつ、20から29個のものはいくつ、などなどを表す リストを生成することである。 ソートした数のリストでは、この処理は簡単である。 まず、10未満の要素数を数え、数え終わった数を飛び越えてから、 20未満の要素数を数え、数え終わった数を飛び越えてから、 30未満の要素数を数え、とすればよい。 10、20、30、40などの各数は、各範囲の最大の数より1大きい。 これらの数のリストをリストtop-of-rangesとしよう。

このリストを自動的に生成することも可能であるが、 書き下したほうが簡単である。 つぎのようになる。

 
(defvar top-of-ranges
 '(10  20  30  40  50
   60  70  80  90 100
  
  
  
  )
 "List specifying ranges for `defuns-per-range'.")

範囲を変更するには、このリストを修正する。

つぎは、各範囲にある定義の数のリストを作る関数を書くことである。 明らかに、この関数は、引数としてsorted-lengthsと リストtop-of-rangesを取る必要がある。

関数defuns-per-rangeは、2つのことを繰り返し行う必要がある。 現在の最大値で指定される範囲内にある定義の数を数えることと、 その範囲内の定義の個数を数え終えたらリストtop-of-rangesのつぎの値に シフトすることである。 これらの各操作は繰り返しであるので、whileループを使ってできる。 1つのループで指定された範囲の定義の個数を数え、 もう一方のループで順番にリストtop-of-rangesのつぎの値を選ぶ。

sorted-lengthsの数個の要素を各範囲において数える。 つまり、小さな歯車が大きな歯車の内側にあるように、 リストsorted-lengthsを処理するループは、 リストtop-of-rangesを処理するループの内側になる。

内側のループでは、各範囲内の定義の個数を数える。 これは、すでに見てきたような簡単な数え上げのループである (See 節 11.1.3 増加カウンタによるループ)。 ループの判定条件では、リストsorted-lengthsの値が範囲の最大値より 小さいかどうかを調べる。 そうならば、関数はカウンタを増やし、 リストsorted-lengthsのつぎの値を調べる。

内側のループはつぎのようになる。

 
(while 長さの要素が範囲の最大値より小さい
  (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)))

まとめると、2つのループはつぎのようになる。

 
(while top-of-ranges

  ;; 現在の範囲内にある要素数を数える
  (while 長さの要素が範囲の最大値より小さい
    (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を使う (See 節 7.2 cons)。

関数consで作れるのだが、 最大の範囲に含まれる定義の個数が先頭になり、 最小の範囲に含まれる定義の個数が最後になる。 これは、consが新たな要素をリストの先頭に置き、 2つのループが長さのリストの最小のものから処理するので、 defuns-per-range-listが最大の数を先頭に置いて終わるからである。 しかし、グラフを表示する際には、最小の値を最初に、最大の値を最後に書きたい。 解決策は、defuns-per-range-listの順番を逆順にすることである。 これには、リストの順番を逆順にする関数nreverseを使う。

たとえば、

 
(nreverse '(1 2 3 4))

は、つぎのようになる。

 
(4 3 2 1)

関数nreverseは「破壊的」である。 つまり、渡されたリストそのものを変更する。 非破壊的な関数carcdrとは対照的である。 ここでは、もとの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)))

関数は、1つの微妙な点を除けば、単純である。 内側のループの判定条件はつぎのようである。

 
(and (car sorted-lengths) 
     (< (car sorted-lengths) top-of-range))

このかわりに、つぎにようにしてみる。

 
(< (car sorted-lengths) top-of-range)

判定条件の目的は、リストsorted-lengthsの先頭要素が、 範囲の最大値より小さいかどうかを調べることである。

簡略した判定条件は、リストsorted-lengthsnilでない限り、 正しく動作する。 しかし、nilであると式(car sorted-lengths)nilを返す。 関数<は、数を空リストであるnilと比較できないので、 Emacsはエラーを通知し、関数の動作を止めてしまう。

リストの最後に達すると、リストsorted-lengthsはつねにnilになる。 つまり、簡略した判定条件の関数defuns-per-rangeを使うと、必ず失敗する。

and式を使って式(car sorted-lengths)を追加して、問題を解決する。 式(car sorted-lengths)は、リストに要素がある限りnil以外の 値を返すが、リストが空になるとnilを返す。 and式は、まず、式(car sorted-lengths)を評価し、 これがnilならば、<式を評価することなく偽を返す。 しかし、式(car sorted-lengths)nil以外の値を返せば、 and式は<式を評価し、その値をand式の値として返す。

このようにして、エラーを防ぐ。 See 節 12.4 forward-paragraph:関数の宝庫, for more information about and.

関数defuns-per-rangeを試してみよう。 まず、リストtop-of-rangesに値の(短かい)リストを束縛する式を評価し、 続いて、リストsorted-lengthsを束縛する式を評価し、 最後に関数defuns-per-rangeを評価する。

 
;; (あとで使うものよりも短いリスト)
(setq top-of-ranges        
 '(
   ))

(setq sorted-lengths
      '(85 86   300))

(defuns-per-range sorted-lengths top-of-ranges)

これはつぎのようなリストを返す。

 
(2 2 2 0 0 1 0 2 0 0 4)

たしかに、リストsorted-lengthsには、110より小さなものは2つあり、 110と119のあいだのものは2つあり、120と129のあいだのものは2つある。 200を超える値のものは4つある。


[ << ] [ >> ]           [表紙] [目次] [索引] [検索] [上端 / 下端] [?]