第24回 プログラミングについて 『JUM の構造』

『JUM の構造』
現在開発しているテキストエディタ JUM の構造の概要は次のようになっています。
キー入力インターフェイス
↓
キーストローク解析
↓
コマンド実行部 → 表示インターフェイス
↑
↓
テキストデータ処理コア
あまりにも大まかな図の上、誰が考えても思い浮かぶ構造です。今回はこの構造の中での各部分の役割分担の関係についてお話します。といっても難しい話ではありませんので安心して下さい。
JUMの開発経緯は以前お話しましたがその第2バージョンでの話です。人間というものは何度同じ体験をしても懲りない愚かなものです。既に何度か説明してきた関数やその集まりの役割分担を熟慮せずに開発を進めていて書き直した部分があります。それが上図のテキストデータ処理コアと表示インターフェイスの関係です。
テキストデータ処理コアは編集中のテキストデータを管理し、削除や挿入などの基本的な処理を行なうテキストエディタで最も核になる部分です。この部分では高度な処理は行ないません。複雑な処理を行なうのはコマンド実行部でコマンドに応じて呼び出された関数が必要に応じてコアの機能を使用するようになっています。
表示インターフェイスはテキストを画面に表示する部分です。
この2人の関係をどうするかが問題だったのです。コアは表示に関してどこまでの情報を持っていなければならないか、また表示する方はコアに関する情報をどこまで持っていればいいのか、この関係付けを誤るとそれ以降に作る関数がとんでもなく面倒なものになってしまう可能性があるのです。下手をするとプログラムが完成しない可能性もあるのです。
初期の頃はコアが現在の表示行の先頭のポインタを保持し、表示部は再描画の要求があればそのポインタの示す行を先頭の行として無条件に1画面分の表示を行なうようにしていました。カーソルの移動時についてもこの表示部が行なうようになっていましたので、当然のことですがちょっとしたカーソルの移動でも1画面分の表示を行ないますので表示速度が遅い上、画面がちらつくことは最初から分かっていました。エディタがある程度の機能を持つようになってから改修を行なっても遅くはないという判断で開発を進めました。
表示速度を上げ、画面のちらつきを押さえるには極力不要な描画を行なわないことです。それではどのようにすれば不要な描画を行なわないで済むかと言えば、現在画面に何を書いてあるのかを記憶しておけばいいのです。がしかしそのためにあまりメモリーを消費したくはありません。本来ならば文字の1つ1つに表示しているか否かのフラグを設定したいところですが、これではメモリーを消費しすぎます。そこで若干おおまかにはなるのですがコアのそれぞれのテキストのデータ(行単位)に表示されているか否かのフラグを持たせることにしました。描画部はコアに描画してあるか否かを問い合わせればいいという算段です。
確かにこの方法はある程度はうまくいきました。このある程度というのが問題で、エディタが各種の編集機能を備えてくるようになると、このフラグの設定が複雑になってしまいすべての状況でこれを行なうには逆にコアの実行速度の低下が問題になってしまいました。
そもそもテキストのデータは行の先頭から連続してメモリー上に配置されている訳ではありません。テキストエディタ(どんなプログラムでも言えることですが)の中でのテキストデータのデータ構造を設計するときには色々な形式が考えられます。
方式1
テキスト
1 行目 □□□□□‥‥‥‥‥□□□□□
2 行目 □□□□□‥‥‥‥‥□□□□□
:
:
Nー1行目 □□□□□‥‥‥‥‥□□□□□
N 行目 □□□□□‥‥‥‥‥□□□□□
│ │
└───── X ──────┘
この方式は予想される最長の行の長さをXとし、また最大の行数をNとする2次元の配列を作っておきテキストのデータを配列の先頭から格納するものです。
この方式では、最大値に制限があること、メモリーの殆どが無駄になること、行の削除や追加のときに大量のメモリーのコピーが発生するのが欠点です。ただし、任意の行を参照する場合は一意に場所が分かるが利点です。
方式2
テキスト
1 行目 □□□□→□‥‥‥‥‥□□
2 行目 □□□□→□‥‥‥‥‥□□□□□
:
:
Nー1行目 □□□□→□‥‥‥‥‥□
N 行目 □□□□→□‥‥‥‥‥□□□□□□□□□□□□
この方式は各行のテキストデータを指し示すポインタを配列とするものです。この方式の場合も方式1と同じに最大行数に制限がありますが、各行の長さは格納するテキストに合わせて調整できますのでメモリーの消費量は格段に低下します。しかしこの場合も行の削除や挿入の再に方式1程ではありませんがメモリーのコピーが発生します。こも方式も行は配列になっているので任意の行は一意に参照できます。
方式3
テキスト
行の先頭を示すポインタ→□□□□→□□
↓
□□□□→□□□□□
↓
□□□□→□□□□□□□□□
↓
□□□□→□□□□
この方式はテキストデータの先頭を指し示すポインタを1つ用意しておき、それがテキストデータの先頭を指し示します。またテキストデータは方式2と同じようにテキストを指し示します。またテキストデータは次の行のテキストデータを指し示します。この方式ではメモリーの無駄は生じません。また行の挿入や削除のときにはポインタのつなぎかえをするだけですので効率は悪くはありません。だた1つ難点は任意の行を直接参照できないことです。
大きくは上にあげた3つの方式のいずれかが考えられますが、これらの派生方式はいくつも考えられるでしょう。基本的にJUMでは方式3の形式を採用しました。ただし次の行を指し示すポインタを前の行も指し示すように双方向とし、データの操作時の効率が低下しないようにしてあります。
この方式で何行目から何行目が表示されているという変数を次のように用意したとします。
int display_from,display_to;
これでは常に各行データの行番号を把握していなければなりませんので、どこからどこまでというのを表示されている行データのアドレスとすると、
struct line_data *display_from,*display_to;
このようになります。がしかしこの方式でも困ったことに行の編集操作を行なうときにこの行データのアドレスが変更されることがあるので、これに応じて表示されている行のポインタを変更するのは結構辛いプログラミングになってしまいます。
それでは各行のデータに表示されているか否かのフラグを次のように設定すれば良いだろうと考えたのです。
struct line_data{
:
:
int displayed;
:
:
};
もし文字の挿入や削除があればこのフラグをオフにし、再画の際にはこのフラグを参照するようにしたのですが、これも行の削除や追加があったときにはフラグの設定部分を作るだけでも大きな苦労となってしまいました。
基本に立ち戻ってよく考えてみると、テキストのデータにこのような情報を持たせること自体が無謀なことなのだと気がついたのです。コアの部分はテキストデータの基本編集機能だけにし、表示に関しては表示インターフェイス部が責任を持って管理すれば話がスッキリするはずです。表示部にはテキストデータのアドレスを意識させず、あくまでも表示している文字列のみを管理すればコア部との関係が密にならずに各々の関数のことだけを気にしてコーディングすればいいことになります。
最終的に、コアは表示するテキストの先頭の行のみを保持することとし、表示部は表示に関することのみに専念するようにプログラムを改修し落ち着く結果となりました。
このように誰が何を行い、どこまでのことをやるのかの切り分けを誤るとプログラムは大変面倒なものになってしまうのです。プログラミングの大切なところは難しくコーディングするのではなく、難しいものを簡単なコーディングで表現することにあります。そのためにもある程度の大きさのプログラムになった場合、関数の機能の切り分けは非常に大切な要素なのです。
今回はちょっと短い話でしたがまた次回。