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

15. グラフの準備

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=eintro&node=Readying%20a%20Graph"
"intro/グラフの準備"へのコメント(無し)

われわれの目標は、Emacs Lispのソースコードにあるさまざまな長さの 関数定義の個数を表示したグラフを作ることである。

実際問題として、グラフを作る場合にはgnuplotなどの プログラムを使うであろう (gnuplotはGNU Emacsとうまく組み合わせることができる)。 しかし、ここでは、ゼロからグラフを描くプログラムを作り、 その過程をとおして、すでに学んだことを復習し、より多くを学ぼう。

本章では、単純なグラフを描く関数をまず書いてみる。 この定義はプロトタイプ(prototype)であり、 素早く書いた関数であるが、グラフ作成という未踏領域の探検を可能にしてくれる。 ドラゴンを発見するか単なる伝説であることを知るであろう。 地形を把握できたら、自動的に軸のラベルを描くように関数を拡張する。

Printing the Columns of a Graph

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=eintro&node=Columns%20of%20a%20graph"
"intro/PrintingtheColumnsofaGraph"へのコメント(無し)

Emacsは柔軟で文字端末を含むいかなる種類の端末でも動作するように設計されている ので、「タイプライタ」文字の1つを使ってグラフを作る必要がある。 アスタリスクがよいであろう。 グラフ表示関数を拡張すれば、 ユーザーのオプションで文字を選択できるようにもできる。

この関数をgraph-body-printとしよう。 唯一の引数としてnumbers-listを取る。 ここでは、グラフにはラベルを付けずに、本体のみを表示することにする。

関数graph-body-printは、numbers-listの各要素ごとに アスタリスクから成る縦のコラムを挿入する。 各コラムの高さはnumbers-listの対応する要素の値で決まる。

コラムの挿入は繰り返し動作なので、 この関数はwhileループや再帰を使って書ける。

第1段階として、アスタリスクのコラムをどのように表示するかを考えよう。 通常、Emacsでは、文字を画面の水平方向に、行単位で表示する。 2つの方法で対処することができる。 独自のコラム挿入関数を書くか、Emacsの既存のものを探すかである。

Emacsに既存のものがあるかどうかを調べるには、コマンドM-x aproposを使う。 このコマンドは、コマンドC-h a(command-apropos)に似ているが、 後者はコマンドとなる関数のみを探す点が異なる。 コマンドM-x aproposは、対話的でない関数も含めて、 正規表現に一致するすべてのシンボルを表示する。

探したいコマンドは、コラムを表示したり挿入するコマンドである。 関数名には、「print」や「insert」や「column」の単語が含まれるであろう。 そこで、M-x apropos RET print\|insert\|column RETとタイプして 結果を見てみよう。 筆者のシステムでは、しばらくしてから、79個の関数や変数を表示した。 この一覧を調べた結果、それらしい唯一の関数はinsert-rectangleであった。 たしかにこれがほしい関数であり、その説明文はつぎのとおりである。

 
insert-rectangle:
Insert text of RECTANGLE with upper left corner at point.
RECTANGLE's first line is inserted at point,
its second line is inserted at a point vertically under point, etc.
RECTANGLE should be a list of strings.

予想どおりに動作するかどうか調べてみよう。

insert-rectangle式の直後にカーソルを置いて C-u C-x C-eとタイプした結果をつぎに示す。 この関数は、ポイントの直後から下向きに `"first"'と`"second"'と`"third"'を挿入した。 また、関数はnilを返した。

 
(insert-rectangle '("first" "second" "third"))first
                                              second
                                              third
nil

もちろん、このinsert-rectangle式自身のテキストをバッファに 挿入したいのではないが、われわれのプログラムからこの関数を呼び出す。 関数insert-rectangleが文字列のコラムを挿入する場所に ポイントを正しく移動しておく必要もある。

Infoで読んでいる場合には、バッファ`*scratch*'などの 別のバッファに切り替え、バッファの適当な場所へポイントを移動し、 M-ESCとタイプして、ミニバッファの問い合わせに insert-rectangle式をタイプしてRETをタイプすれば、 この動作を調べることができる。 これにより、Emacsはミニバッファの式を評価するが、 ポイントの値としては、バッファ`*scratch*'のポイントの位置を使う (M-ESCは、eval-expressionのキーバインドである)。

ポイントは最後に挿入した行の直後に移動していることがわかる。 つまり、この関数は、副作用としてポイントを移動する。 この位置でコマンドを繰り返すと、直前の挿入位置の右に下向きに挿入される。 これでは困る!  棒グラフを描くときには、コラムが互いに並んでいる必要がある。

コラムを挿入するwhileループの各繰り返しでは、ポイントを移動して コラムの最後ではなくコラムの先頭に置く必要があることがわかる。 さらに、グラフを描くとき、すべてのコラムが同じ高さではない。 つまり、各コラムの先頭は、直前のものとは異なった高さにある。 単純にいつも同じ行にポイントを位置決めすることはできず、 正しい位置に移動する必要がある。 たぶんこうできるだろう...

アスタリスクで表した棒グラフを描きたいのである。 各コラムのアスタリスクの個数は、numbers-listの要素で決まる。 insert-rectangleの各呼び出しでは、 正しい長さのアスタリスクのリストを作る必要がある。 必要な個数のアスタリスクだけでこのリストが作られている場合、 グラフを正しく表示するには、基準行から正しい行数だけポイントを上に 位置決めする必要がある。 これは、難しい。

かわりに、つねに同じ長さのリストをinsert-rectangleに渡すことができれば、 新たにコラムを追加するたびに右へ移動する必要はあるが、 ポイントは同じ行に置ける。 このようにした場合、insert-rectangleに渡すリストの一部は アスタリスクではなく空白にする必要がある。 たとえば、グラフの最大の高さが5で、コラムの高さが3だとすると、 insert-rectangleにはつぎのような引数が必要になる。

 
(" " " " "*" "*" "*")

コラムの高さがわかれば、このようなことは難しくない。 コラムの高さを指定する方法は2つある。 適当な高さをあらかじめ指定しておけば、その高さまでのグラフは正しく描ける。 あるいは、数のリストを調べて、リストの最大値をグラフの最大の高さとする。 後者の処理が難しければ、前者の処理がもっとも簡単である。 Emacsには引数の最大値を調べる組み込み関数がある。 その関数を使おう。 関数はmaxであり、数である全引数の中の最大値を返す。 たとえば、

 
(max  3 4 6 5 7 3)

は7を返す (対応する関数minは、全引数の中の最小値を返す)。

しかし、単純にnumbers-listに対してmaxを呼べない。 関数maxは、引数として数のリストではなく数を要求する。 したがって、つぎの式、

 
(max  '(3 4 6 5 7 3))

は、つぎのようなエラーメッセージを出す。

 
Wrong type of argument:  integer-or-marker-p, (3 4 6 5 7 3)

引数のリストを関数に渡す関数が必要である。 この関数は、第1引数(関数)を残りの引数に「適用(applies)」する。 なお、最後の引数はリストでもよい。

たとえば、

 
(apply 'max 3 4 7 3 '(4 8 5))

は、8を返す。

(本書のような書籍なしで、この関数をどのように学ぶのか筆者にはわからない。 関数名の一部を予想してaproposを使えば、 search-forwardinsert-rectangleなどのこれ以外の関数を 探すのは可能である。 第1引数を残りに「適用(apply)」するという隠喩は明らかであるにも関わらず、 初心者がaproposや他の補佐機能を使うときに、この用語を思い付くとは 思えない。 もちろん、筆者がまちがっている可能性もあるが、 いずれにしても、関数は、それを最初に発明した人が命名する。)

applyの2番目以降の引数は省略できるので、 リストの要素を渡して関数を呼び出すためにapplyを使える。 つぎのようにしても「8」を返す。

 
(apply 'max '(4 8 5))

applyをこの方法で使うことにする。 関数recursive-lengths-list-many-filesは、 maxを適用する数のリストを返す (ソートした数のリストにmaxを適用することもできる。 リストがソートされているかいないかは関係ない)。

したがって、グラフの最大の高さを調べる操作は、つぎようになる。

 
(setq max-graph-height (apply 'max numbers-list))

グラフのコラムを表す文字列のリストの作り方に戻ろう。 グラフの最大の高さとコラムに現れるべきアスタリスクの個数を与えられて、 関数はコマンドinsert-rectangleで挿入すべき文字列のリストを返す。

各コラムは、アスタリスクか空白文字である。 関数はコラムの高さの値とコラム内のアスタリスクの個数を与えられるので、 空白の個数は、コラムの高さからアスタリスクの個数を引けば計算できる。 空白の個数とアスタリスクの個数を与えられ、 2つのwhileループでリストを作る。

 
;;; 第1版
(defun column-of-graph (max-graph-height actual-height) 
  "Return list of strings that is one column of a graph."
  (let ((insert-list nil)
        (number-of-top-blanks
         (- max-graph-height actual-height)))

    ;; アスタリスクを埋める
    (while (> actual-height 0)                
      (setq insert-list (cons "*" insert-list))
      (setq actual-height (1- actual-height)))

    ;; 空白を埋める
    (while (> number-of-top-blanks 0) 
      (setq insert-list (cons " " insert-list))
      (setq number-of-top-blanks
            (1- number-of-top-blanks)))

    ;; リスト全体を返す
    insert-list))

この関数をインストールして、つぎの式を評価すれば、 目的のリストが返されることがわかる。

 
(column-of-graph 5 3)

は、つぎのリストを返す。

 
(" " " " "*" "*" "*")

このcolumn-of-graphには1つの大きな欠陥がある。 コラムの空白や印として使うシンボルを、空白文字とアスタリスクに 「書き込んである(hard-coded)」ことである。 プロトタイプとしてはよいが、別のシンボルを使いたい人もいるだろう。 たとえば、グラフ関数をテストするときには、空白のかわりにピリオドを使って、 関数insert-rectangleを呼ぶたびにポイントが正しくなっていることを 確かめたい。 あるいは、アスタリスクのかわりに`+'や別の記号を使いたいであろう。 コラム幅を1文字より大きくしたい場合もあろう。 プログラムはより柔軟であるべきである。 これには、空白文字とアスタリスクのかわりに、 2つの変数、graph-blankgraph-symbolを使い、 これらの変数に別々に値を定義する。

また、説明文も十分ではない。 これらを考慮すると、つぎの第2版になる。

 
(defvar graph-symbol "*"
  "String used as symbol in graph, usually an asterisk.")

(defvar graph-blank " "
  "String used as blank in graph, usually a blank space.
graph-blank must be the same number of columns wide
as graph-symbol.")

defvarの説明は、 8.4 defvarによる変数の初期化を参照。)

 
;;; 第2版
(defun column-of-graph (max-graph-height actual-height) 
  "Return list of MAX-GRAPH-HEIGHT strings; 
ACTUAL-HEIGHT are graph-symbols.
The graph-symbols are contiguous entries at the end 
of the list.
The list will be inserted as one column of a graph.  
The strings are either graph-blank or graph-symbol."

  (let ((insert-list nil)
        (number-of-top-blanks
         (- max-graph-height actual-height)))

    ;; graph-symbolsを埋め込む
    (while (> actual-height 0)                
      (setq insert-list (cons graph-symbol insert-list))
      (setq actual-height (1- actual-height)))

    ;; graph-blanksを埋め込む
    (while (> number-of-top-blanks 0) 
      (setq insert-list (cons graph-blank insert-list))
      (setq number-of-top-blanks
            (1- number-of-top-blanks)))

    ;; リスト全体を返す
    insert-list))

必要ならば、column-of-graphをもう一度書き直して、 棒グラフにするか折線グラフにするを決めるオプションを 与えられるようにもできる。 これは難しくはない。 折線グラフは、各バーの先頭より下が空白の棒グラフであると考えられる。 折線グラフのコラムを作るには、まず、値より1小さい空白文字のリストを作り、 consを使って印のシンボルをリストに繋げ、 consを使ってリストの先頭に空白文字を埋め込む。

このような関数の書き方は簡単であるが、 われわれには必要ないのでやらないことにする。 しかし、そのようにするならばcolumn-of-graphを書き直す。 より重要なことは、別の部分には何の変更も必要ないことに注意してほしい。 強調するが、やろうと思えば簡単にできる。

では、グラフを描く最初の実際の関数を書いてみよう。 これはグラフの本体を描くが、垂直軸や水平軸のラベルを描かないので、 この関数をgraph-body-printと呼ぶことにする。



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

15.1 関数graph-body-print

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

前節までの準備があるので、関数graph-body-printは簡単である。 この関数は、数のリストの各要素が各コラムのアスタリスクの個数を指定するものと して、アスタリスクや空白文字から成るコラムを描く。 これは繰り返し動作なので、減少方式のwhileループや再帰的関数で書ける。 本節では、whileループを使った定義を書こう。

関数column-of-graphは、引数としてグラフの高さが必要であるので、 これをローカル変数とする。

この関数のwhileループの雛型はつぎのようになる。

 
(defun graph-body-print (numbers-list)
  "説明文..."
  (let ((height  ...
         ...))

    (while numbers-list
      コラムを挿入し、ポイントを再位置決めする
      (setq numbers-list (cdr numbers-list)))))

この雛型の項目を埋めていこう。

グラフの高さを求めるには、式(apply 'max numbers-list)を使う。

whileループは、numbers-listの要素を一度に1つずつ処理する。 リストを短くするには式(setq numbers-list (cdr numbers-list))を使う。 リストのCARはcolumn-of-graphの引数である。

whileの各繰り返しでは、関数insert-rectangleで、 column-of-graphが返したリストを挿入する。 関数insert-rectangleは、挿入位置の右下にポイントを移動するので、 挿入するときのポイントの値を保存し、挿入後にポイントを戻し、 つぎにinsert-rectangleを呼び出すために水平方向に移動する。

挿入するコラムが1文字幅ならば、 再位置決めコマンドは単に(forward-char 1)でよい。 しかし、コラムの幅が1文字を越えるかもしれない。 つまり、再位置決めコマンドは(forward-char symbol-width)とすべきである。 symbol-widthは、graph-blankの長さであり、 式(length graph-blank)で調べる。 変数symbol-widthをコラム幅にバインドする最適の場所は、 let式の変数リストである。

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

 
(defun graph-body-print (numbers-list)
  "Print a bar graph of the NUMBERS-LIST.
The numbers-list consists of the Y-axis values."

  (let ((height (apply 'max numbers-list))
        (symbol-width (length graph-blank))
        from-position)

    (while numbers-list
      (setq from-position (point))
      (insert-rectangle
       (column-of-graph height (car numbers-list)))
      (goto-char from-position)
      (forward-char symbol-width)
      ;; コラムごとにグラフを描く
      (sit-for 0)               
      (setq numbers-list (cdr numbers-list)))
    ;; 水平軸のラベル用にポイントを置く
    (forward-line height)
    (insert "\n")
))

この関数定義で予期しなかった式は、 whileループの中の式(sit-for 0)である。 この式は、グラフ表示操作を見ているとおもしろくする。 この式は、Emacsに「じっと(sit)している」ように、つまり、 0時間のあいだ何もしないで、画面を再描画させる指示である。 ここに書くことで、Emacsはコラムごとに画面を再描画する。 これがないと、関数が終了するまでEmacsは画面を再描画しない。

数の短いリストでgraph-body-printを試そう。

  1. graph-symbolgraph-blankcolumn-of-graphgraph-body-printをインストールする。

  2. つぎの式をコピーする。

     
    (graph-body-print '(1 2 3 4 6 4 3 5 7 6 5 2 3))
    

  3. バッファ`*scratch*'に切り替え、 グラフを描き始める位置にカーソルを置く。

  4. M-ESC(eval-expression)とタイプする。

  5. ミニバッファにC-yyank)でgraph-body-print式をヤンクする。

  6. RETを押してgraph-body-print式を評価する。

Emacsはつぎのようなグラフを描く。

 
                    *    
                *   **   
                *  ****  
               *** ****  
              ********* *
             ************
            *************



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

15.2 関数recursive-graph-body-print

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

関数graph-body-printは、再帰的に書くこともできる。 この場合には、2つの部分に分ける。 グラフの最大の高さなどの一度だけ探す必要がある変数の値を決めるための let式を使った外側の「呼び出し側関数(wrapper)」と、 グラフを書くために再帰的に呼び出される内側の関数である。

「呼び出し側関数」は簡単である。

 
(defun recursive-graph-body-print (numbers-list)
  "Print a bar graph of the NUMBERS-LIST.
The numbers-list consists of the Y-axis values."
  (let ((height (apply 'max numbers-list))
        (symbol-width (length graph-blank))
        from-position)
    (recursive-graph-body-print-internal
     numbers-list
     height
     symbol-width)))

再帰関数は少々込み入っている。 これには4つの部分がある。 「再帰条件」、コラムを書くコード、再帰呼び出し、「次段式」である。 「再帰条件」はif式であり、numbers-listに要素が 残っているかどうかを調べる。 そうならば、書くコードを使って1つのコラムを書き、自身を再度呼び出す。 関数が自身を再度呼び出す場合には、「次段式」で短くした numbers-listの値を使う。

 
(defun recursive-graph-body-print-internal
  (numbers-list height symbol-width)
  "Print a bar graph.
Used within recursive-graph-body-print function."

  (if numbers-list
      (progn
        (setq from-position (point))
        (insert-rectangle
         (column-of-graph height (car numbers-list)))
        (goto-char from-position)
        (forward-char symbol-width)
        (sit-for 0)     ; コラムごとにグラフを描く
        (recursive-graph-body-print-internal
         (cdr numbers-list) height symbol-width))))

インストールしてから試してみよう。 たとえばつぎのようにする。

 
(recursive-graph-body-print '(3 2 5 6 7 5 3 4 6 4 3 2 1))

recursive-graph-body-printはつぎのような出力をする。

 
                *        
               **   *    
              ****  *    
              **** ***   
            * *********  
            ************ 
            *************

これら2つの関数、graph-body-printrecursive-graph-body-printの いずれも、グラフの本体を作る。



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

15.3 軸表示の必要性

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=eintro&node=Printed%20Axes"
"intro/軸表示の必要性"へのコメント(無し)

グラフを読み取るためには、グラフの軸が必要である。 一度だけならば、Emacsのピクチャーモードを使って、 手で軸を描くのも合理的であろう。 しかし、グラフ描画関数は何回も使われる。

このため、基本の関数print-graph-bodyを拡張して、 水平軸と垂直軸のラベルを自動的に描くようにした。 ラベル表示関数には新しいことがらはないので、のちほど付録で説明する。 See 節 C. ラベル付きグラフ



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

15.4 演習問題

URL="https://bookshelf.jp/cgi-bin/goto.cgi?file=eintro&node=Line%20Graph%20Exercise"
"intro/演習問題"へのコメント(無し)

折線グラフを描くグラフ表示関数を書いてみよ。


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