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

15. グラフを描く準備

我々の目標は、Emacs のソースの中の様々な長さの関数定義の数をグラフにして 表示することであった。

実際には、グラフを生成するには gnuplot のようなプログラムを使って いることだと思う。(gnuplot は GNU Emacs と相性がよい。)しかしなが ら、今回は一からプログラムを書いていくことにする。そしてその過程で、今ま で学んできた事柄を再度確認しつつ、新しいことも学んでいくことにしよう。

この章では、まずグラフを表示する単純な関数を書いてみる。最初の定義は 雛型 (prototype) であり、グラフを作成するという未知の領域を 偵察するために取り敢えず書いてみるといった類のものである。我々はドラゴン を発見するかもしれないし、あるいはそれが単なる神話であることが分るかもし れない。ともかく地理感覚が掴めてしまえば、自信もつくし、関数を拡張して軸 に自動的にラベルをつけることも出来るようになるだろう。

棒グラフの表示  
15.1 関数 graph-body-print  グラフ本体の表示
15.2 関数 recursive-graph-body-print  グラフ本体の表示を再帰的に行う
15.3 軸を表示する  
15.4 練習問題  

棒グラフの表示

Emacs はどんな種類のターミナルでも動作するよう柔軟に設計されている。その 中にはキャラクターだけしか表示出来ないターミナルも含まれているので、グラ フは「タイプライタ」の文字から出来ている必要がある。取り敢えずはアスタリ スクを使うのがよいだろう。後から関数を拡張して、この文字をユーザーが選択 出来るようにすることも可能だ。

この関数を、graph-body-print と呼ぶことにしよう。これは引数として numbers-list を取る。現段階ではグラフのラベルは出力せず、本体部分 だけを表示することにする。

graph-body-print 関数は、numbers-list の各々の値に対して、 アスタリスクを垂直に並べて表示する。それぞれの高さは、 numbers-list の各要素の値によって決まる。

アスタリスクを垂直に並べる動作は繰り返しである。従って、while ルー プか再帰を使って書くことが出来る。

最初の困難は、如何にしてアスタリスクを縦に並べたものを表示するかである。 普通、Emacs 上でタイプすると、文字はスクリーン上に水平に、行ごとに表示さ れていく。解決への道は二通りある。一つは自分で垂直に挿入するような関数を 書くこと、もう一つは元々 Emacs にそのような関数がないか探すことである。

Emacs に特定の機能を持ったものがないか探す場合には、M-x apropos コ マンドを使うことが出来る。このコマンドは、C-h a (command-apropos) コマンドとほぼ同様なのだが、command-apropos の方はコマンドだけしか検索し ない。M-x apropos の方は、インタラクティブでないものも含めて正規表 現にマッチするものは全てリストしてくれる。我々が探しているものは縦の文字 列 (column) を表示 (print) したり挿入 (insert) したりするコマンドである。 可能性としては、そのような関数は `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.
 
(日本語訳)
RECTANGLE のテキストを左上がポイントに来るような位置に挿入する。 
つまり RECTANGLE の最初の行がポイントの位置に挿入され、
二行目はポイントの真下の位置に挿入され、というふうになる。
RECTANGLE は文字列のリストでなければならない。

簡単なテストを行って、これが本当に求めるものかを確かめてみよう。

以下が、insert-rectangle 式の直後にカーソルを持っていって、 C-u C-x C-e (eval-last-sexp) とタイプしてみた結果である。 この関数は、`"first"', `"second"' そして `third"' をポイ ントの下に表示する。関数全体としては、nil が返る。

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

(訳註:広く配布されている Mule 2.3 (Emacs version 19.28) では、この関数 にバグがある。これは、`lisp/rect.el' の最後の方にある move-to-column-strictly の関数定義の中で、四行目の clm(progn (if force (indent-to column)) column) で置換えることで修正 出来る。)

勿論、我々は insert-rectangle 式そのものをグラフを描くバッファに 挿入したいのではない。そうではなく、我々のプログラムからこの関数を呼び出 したいのである。ただその際、ポイントがバッファの中で、ちゃんと insert-rectangle 関数で縦の文字列を挿入すべき位置にあるかどうかを 確かめなければならない。

もしこの文章を Info の中で読んでいるなら、まず `*scratch*' などの別 バッファに移り、ポイントをバッファのどこかに置きつつ typing M-:と タイプしよう。(訳註:1998 年現在、日本でまだ使用が多いと思われる Emacs 19.28 ベースの Mule では M-ESC である。同様に多い Emacs 19.34 べースのものでは M-: で良い。M-ESC はプレフィク スキーとして使われるようになった。) 続けてミニバッファで insert-rectangle 式をタイプしてやれば、このような動作が可能である ことが分る。ここでは、S式を評価するのはミニバッファの中だったのだが、そ の際のポイントの値としては、`*scratch*' バッファの中のポイントの値 が使われたのであった。(M-:eval-expression のキーバイン ディングである。)

実際にやってみると、ポイントが直前に入力した行の最後にある状態で終わるこ と---つまり、この関数は副作用として、ポイントを移動することが分る。この コマンドを続けて実行すると、次の挿入は前回の位置から下方及び右方向に移動 した所から行われる。これでは困る。棒グラフを作成するには、縦の列が隣同士 に並ばなければならない。

というわけで、棒グラフを挿入する while ループの各々のサイクルで、 ポイントの位置を適切な位置に再配置する必要があることが分った。また、その 位置は縦の列の一番上であって底ではない。更に、このグラフを表示する際に、 個々の縦の列の長さが全て揃うことはまずない。つまり、各々の縦棒のてっぺん の高さは、前のものと異なるのが普通である。従って、単に前と同じ行にポイン トを移動していくだけでは駄目である。そうではなくて...

我々はグラフをアスタリスクで表示しようとしていたのであった。アスタリスク の数は numbers-list の現在の要素で指定される。このアスタリスクを 要素とするリストを、insert-rectangle を呼び出すたびに作成する必要 がある。もし、このリストが単に必要な数のアスタリスクだけからなっていたと すると、ポイントの位置をグラフの基準線から正しい行数だけ上の位置におかな ければならない。しかし、これは難しい。

その代わり、もし insert-rectangle に対して常に一定の長さのリスト を渡すことが出来れば、ポイントも常に同じ行の一つ右の桁に移動すればよくな る。しかし、この場合には insert-rectangle に渡されるリストのいく つかの要素はアスタリスクではなく空白になる。例えば、もしグラフの最大の高 さが5であり、現在の高さが3であったとすると、insert-rectangle は、 次のような引数を必要とする。

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

この最後の方法は、縦の列の高さを決定出来さえすれば、それほど難しくはない。 縦の列の高さを特定するには二つの方法がある。一つは我々が綺麗に表示される ような高さを勝手に指定してしまうというもの、もう一つは、数のリストを検索 して、最大のものをグラフの最大の高さとして使うというものである。後者の方 法が難しい場合は、前者を採用するのが楽である。が、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:  number-or-marker-p, (3 4 6 5 7 3)

従って、リストの中身を引数として関数に渡すような関数が必要になる。 apply と呼ばれる関数がこの役目を果してくれる。この関数はその最初 の引数である関数に、残りの引数を適用 (apply) する。この時、最後の引数は リストでなければならない。

例えば

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

は8を返す。

(ついでにいうと、私はこの関数をこの文書のようなものを使わずに見つけだす 方法を知らない。他の search-forwardinsert-rectangle と いった関数ならば、名前の一部分を推測して apropos を使えばよい。け れども apply については、名前の元となるメタファ---最初の引数に残りを `apply' する---は明らかであるにしても、初心者が apropos や他の助 けを借りてこのような特定な単語に辿り着けるとは思わない。勿論、私が間違っ ているのかもしれない。結局は、関数はそれを作った人によって名付けられるも のなのである。)

apply の二番目以降の引数は省略可能である。従って、apply を 使って、関数にリストの要素を引数として渡して呼び出すことも出来る。例えば、 次は8を返す。

 
(apply 'max '(4 8 5))

この最後の例が我々の目的にあっている。 recursive-lengths-list-many-files 関数は、数のリストを返すが、こ れを max に適用することが出来るわけである。(max にソートさ れたリストを渡すことも出来るが、この場合はソートされていてもされていなく ても関係ない。)

というわけで、グラフの最大の高さを求める操作は次の通りである。

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

では、グラフの縦の列を描くための文字列のリストをどうやって作ればよいかと いう問題に戻ろう。この関数はグラフの最大の高さと個々の縦の列の中のアスタ リスクの数から、insert-rectangle コマンドが挿入する文字列のリスト を返す必要がある。

各々の縦の列は空白とアスタリスクからなる。高さとアスタリスクの数が分れば、 空白の数はその差として求められる。そして空白とアスタリスクの数が分れば、 二つの while ループを使ってリストを作ることが出来る。

 
;;; 最初のバージョン
(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))

この関数をインストールしてから次のS式を評価すると、求めるリストが得られ るはずだ。

 
(column-of-graph 5 3)

を評価すると、

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

が返されるというわけである。

前にも書いたが、column-of-graph には大きな欠陥がある。空白とグラ フ本体の印のために用いられる記号はスペースとアスタリスクに「ハードコード」 されている。これは雛型としてはよい。が、あなたや他のユーザは他の記号を使 いたいと思うことも多いだろう。例えば、グラフ関数をテストしてみる際には、 insert-rectangle 関数が呼ばれた時にポイントの位置がきちんと移動さ れているかどうかを見るために、スペースの代わりに終止符を使いたいと思うだ ろう。また、アスタリスクの代わりに `+' 等の記号を使ったりしたいと思 うこともあるに違いない。更に、グラフの桁数をディスプレイの一桁の幅よりも 広く取りたいと思うこともあるだろう。そういうわけでプログラムはもっと柔軟 であるべきである。そのための方法としては、スペースとアスタリスクを graph-blankgraph-symbol という二つの変数で置き換えて、 これらの変数を別に定義することが考えられる。

また、説明文字列も解りやすいとは言えない。以上のことを考慮すると、次のよ うな関数に辿り着く。

 
(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 の説明については、defvar を用いた変数の初期化,を参照。)

 
;;; 二番目のバージョン
(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 も書き換えて、オプションとして棒グラ フだけでなく、線グラフも書けるようにも出来る。これはそんなに難しいことで はない。線グラフをどう描くかだが、例えば棒グラフにおいて、一番上の点から 下は全て空白にしてしまえば、それはもう棒グラフとは呼べないだろう。線グラ フのための縦の文字列を作るには、まずは値よりも一つだけ少ない数の空白のリ ストを作り、ついで cons を用いてリストにグラフ記号を追加し、最後 に、上の余白部分の数の空白を付け足せばよい。

こういう関数を実際に書くのも簡単だが、今の所は必要ないので、書かないでお くことにする。が、ともかく書くことは出来るし、またこの関数を一度書いてし まえば、column-of-graph で使うことも出来る。ここで大切なことは、 他の部分の書き換えは殆どしなくても良いということである。即ち、拡張しよう と思えば簡単に出来るわけだ。

さて、やっと、はじめて実際のグラフを書いてみる所まで来た。ここではグラフ の本体だけを表示し、縦軸や横軸のラベルは表示しない。そこで、この関数を graph-body-print と呼ぶことにする。


15.1 関数 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 の各要素に対して一回ずつ回る。 このリストは (setq numbers-list (cdr numbers-list)) 式によって短 くなっていき、各時点でのリストの CAR が column-of-graph に引 数として渡される。

この while ループの各サイクルで、column-of-graph によって 返されたリストが insert-rectangle 関数に渡される。 insert-rectangle 関数はポイントを挿入された矩形の右下のポイントに 移動するので、それを矩形が挿入される前の位置に戻してから、次の位置に水平 方向に移動してやる必要がある。そこで次の insert-rectangle が呼ば れるわけである。

単独の空白とアスタリスクを使った場合などのように、もし挿入される棒グラフ が一桁の幅ならば、移動のためのコマンドは単に (forward-char 1) と なる。しかし、この幅はもっと大きくなるかもしれない。従って、 (forward-char symbol-width) と書く方が良い。symbol-widthgraph-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)))
    ;; X 軸のラベルのためにポイントを移動
    (forward-line height)
    (insert "\n")
))

この関数では、ひとつ予期していなかったS式が出てくる。それは while ループの中の (sit-for 0) 式である。この式を使う ことで、グラフを表示する過程を見るのが楽しくなる。この式は Emacs に 0 時 間待ってから画面を再描画させるものである。これを上の位置に置くと棒グラフ が一つずつ描かれていくことになる。逆に置かなければ、関数が仕事を全て終了 するまでグラフは描かれない。

次のようにすれば、この graph-body-print を短いリストに対してテス トしてみることが出来る。

  1. まず graph-symbolgraph-blankcolumn-of-graph そ して graph-body-print をインストールする。

  2. 次のS式をコピーする。

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

  3. `*scratch*' バッファに移り、グラフを表示させたい位置にカーソルを置 く。

  4. M-: (eval-expression) とタイプする。(訳註:Emacs 19.28 ベー スの Mule では、M-ESC.)

  5. C-y (yank) を使って graph-body-print 式をミニバッファ に yank する。

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

Emacs は次のようなグラフを表示するはずである。

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


15.2 関数 recursive-graph-body-print

graph-body-print 関数は、再帰を使って書くことも出来る。この場合は、 二つの部分に分けて書くことになる。外側の関数 (wrapper) で 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)))

再帰関数の方は、ちょっとばかり難しい。これは `do-again-test'、グラフ表示 コード、再帰呼び出し、そして `next-step-expression' の四つの部分からなる。 `do-again-test' は number-list にまだ要素が残っているかを判定する if 式である。もし残っていれば、一本の棒グラフを書いてからまた自 分自身を呼び出す。関数が自分自身をもう一度呼び出すかどうかは、結局は `next-step-expression' が返す値による。これは、短縮版の number-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))

これを評価すると、次のようなグラフが描かれる。

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

graph-body-printrecursive-graph-body-print もグラフの 本体部分のみを描く関数である。


15.3 軸を表示する

グラフには、それが何を表わすかを示すために軸を表示する必要がある。一度だ けしかグラフを描かないのならば、Emacs の Picture mode を利用して手で軸を 描くのも結構である。しかし、この関数は何度も利用するかもしれない。

というわけで、基本的な print-graph-body 関数を拡張して、自動的に 横軸と縦軸のラベルを表示するようにしてみた。この関数には特に新しい事柄は 含まれていないので、これについての説明は次の所ですることにする。 ラベルと軸が付いたグラフ.


15.4 練習問題

上と同様なグラフを表示する関数で、横棒のグラフを描くバージョンを作りなさい。


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

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