2010-09-18

Gauche でフィルタを書く

前回、Lisp で以下のような「行指向パターンマッチ方式」でフィルタを書くのは難しい、と書いた。

(行指向パターンマッチ方式)
while (入力がある) {
    一行読み込む;
    if (あるパターンに一致する) {
        一行分のデータに対して処理を行う;
        結果を出力する;
    } else if (別のパターンに一致する {
        別の処理を行う;
        結果を出力する;
    } ...
}

その代わりにリストを利用した「丸ごと読み込んで、全部処理して、まとめて書き出す」方式ならすっきりと書ける、と。

後者は確かにその通り。「プログラミングGauche」のコラム『「Lisp 脳」の謎に迫る - Scheme プログラマの発想』を読んでも、入力をリストにして、リストの各要素を処理して、結果をリストにする、というのは Lisp らしい書き方なんだとわかる。

けれど、前者は間違いだった。Scheme でも大して難しくはないと気付いた。

while ループは条件分岐と goto で書き直せる

説明を単純にするためにパターンマッチのない「行指向方式」に戻ろう。

(行指向方式)
while (入力がある) {
    一行読み込む;
    一行分のデータに対して処理を行う;
    結果を出力する;
}

これは、以下のように書き直すことができる。

(行指向方式 #2)
Loop:
    if (入力がない) goto Exit;
    一行読み込む;
    一行分のデータに対して処理を行う;
    結果を出力する;
    goto: Loop;
Exit:

こうなれば、Scheme でも容易に実現できることがわかる。なぜなら、Scheme では、

(「プログラミングGauche」p.6 より)

  • 手続き呼び出しは継続を伴った引数つき goto である

からだ。また、(もう少しわかりやすく)こうも書いてある。

(「プログラミングGauche」p.57 より)
処理の一番最後に再帰呼び出しをして、その結果がそのまま現在の処理の結果として返されるパターンを末尾再帰と呼びます[...snip...]

Scheme では、上記のとおり、最後に呼び出した手続きの戻り値に何も行わない場合、最後の呼び出しは通常の手続き呼び出しではなくジャンプとして扱われることになっています

末尾再帰を goto 代わりに使う

手続き呼び出しが goto なのだとわかれば、上述の「行指向方式 #2」はこう書くことができる。

 1: (define (apply-filter proc port)
 2:   (let ((line (read-line port)))
 3:     (if (not (eof-object? line))
 4:       [begin
 5:         (proc line)
 6:         (apply-filter proc port)])))

3 行目がループの脱出条件の判定に、6 行目の再帰呼び出しがループの先頭に戻る goto に相当する。proc に標準入力ポート、proc にデータを処理する手続きを指定して、apply-filter を呼び出せばフィルタプログラムができあがる。

以下は、これを利用して書いた wc っぽいフィルタだ。単語数の出力が wc と同じにならないことがあるのは単語の区切りとして空白文字しか想定していないためだ。

「BEGIN」と「END」というコメントは AWK をちょっと思い出して書いてみた。たぶん、AWK でこれと同じものを書くとすると、やはり BEGIN でグローバル変数を用意し、END で結果を出力する、というようになると思う。

「まとめて」と「少しずつ」の違い

正規表現を使った処理を scheme (Gauche) で書いてみる」で示したものは、読み込み、処理、結果出力の 3 つの段階をそれぞれ「まとめて」実行する方式だ。一方、↑で示した wc モドキでは、3 つの段階を「少しずつ」行い、全体をループするようになっている。

データ量が小さいときには、両者の方式の実行に差を見つけることは難しいだろう。実際のところ、いまどきの Mac や PC ならどちらの方式で書いてあろうが、たいていの入力に対して一瞬で完了するはず。ただ、データ量がぐっと大きくなるとその差が顕在化するはず。

極端なことを言えば、入力ファイルが GB 単位の大きさになると「まとめて」方式は(いまどきのパソコンでも)かなり苦しくなる。すべてを一度にメモリに読み込む必要があるからだ。32 ビットモードなプラットフォームなら読み込みの段階で実行時エラーになるかもしれない。一方「少しずつ」方式では、データ量が大きくなっても実行時に必要なメモリは一定の範囲に収まる。もちろん実行時間はデータ量に見合うだけのものが必要になるが、データが読み込めないというような事態にはならないはず。ま、もっとも、GB 単位のデータ(それもテキスト)を処理するなんてことは、かなり特殊な状況で、普通の人は一生、経験することはないかもしれない。

実のところフィルタにもいろいろあって、「まとめて」方式では実現できても「少しずつ方式」では実現できなかったり、難しかったりするものもある。たとえば、sort は「まとめて」なら実装しやすくて、「少しずつ」だと困難な(というか外部にデータを書き出さない限りできないよね?)フィルタだ。trgrep は、まさに「行指向パターンマッチ方式」に適した例だし、wcuniq あたりは実装に工夫(状態の保持にグローバルなデータ構造を使うとか)が必要になる例だろう。

参考文献

プログラミングGauche
Kahuaプロジェクト
オライリージャパン ( 2008-03-14 )
ISBN: 9784873113487
おすすめ度:アマゾンおすすめ度

関連リンク

関連記事

twitter より (2010-09-17)

Powered by twtr2src.

2010-09-17

フィルタプログラムとは

前回書いた「かけら」をもっと Scheme/Lisp らしくすることを考える前に、題材になっているフィルタとはどんなプログラムなのかを復習しておこう。

Unix とフィルタ

以下の一文はフィルタと呼ばれる一群のプログラムの特徴をうまく表現している。

(「UNIXプログラミング環境」p.155 より)
UNIX には,"何か入力を読み込み,それに単純な変形を加え,何らかの出力を書き出す" という仕事をするプログラムが多数存在する.例をあげると,入力の一部分を選び出す greptail,入力をソートする sort,入力のなかのワード数を数える wc がある.このようなプログラムを総称してフィルタと読んでいる.

また、フィルタは単独で使われるだけでなく、複数を組み合わせることでより複雑な処理を実現することもできる。

(「UNIXプログラミング環境」p.199 より)
わずか 1 つのフィルタを適用してやることで抱えた問題が氷解することもあるが,複数のフィルタを結合して 1 本のパイプラインにすることは、問題を解決可能な小問題に分解するのに役立つ.このようなツールの利用法こそは UNIX プログラミング環境の核心であるといわれることが多い.

Unix のシェルが備えているパイプやリダイレクトは、フィルタを組み合わせるための仕組みだ。

設計パターン

パイプやリダイレクトのような仕組みでつなげられるようにするには、それぞれのフィルタプログラムが共通する設計を持っていなければならない。それが以下の 3 つだ(厳密には最初の 2 つで十分)。

  • 共通の入力方式
  • 共通の出力方式
  • 上記入出力とは独立したメッセージ出力方式

Unix (とそれに良く似たプログラミング環境)では、上記の 3 つは、それぞれ標準入力、標準出力、そして標準エラー出力と呼ばれている。

簡単に言えば、フィルタプログラムとは、標準入力からデータを読み込み、適切な処理をした後、標準出力に結果を出力し、処理中に必要があれば(警告メッセージの表示など)標準エラー出力に書き出すようなもののことだ。

実装パターン

フィルタを実装するために汎用パターンは以下のようになる。

(汎用方式)
すべての入力を読み込む;
入力に対して処理を行う;
結果を出力する;

この場合、一般には、入力、処理、出力のそれぞれの段階でループが必要で、データを蓄えておく領域も入力と出力にそれぞれ必要になる。はっきり言えば、この方式は効率が良くない。

処理の単位が文字(バイト)ごとだとわかっていれば、以下のように入力、処理、出力を共通のループに押し込む方式が使える。

(文字指向方式)
while (入力がある) {
    一文字読み込む;
    一文字に対して処理を行う;
    結果を出力する;
}

こうすればループの回数は減るし、必要な記憶領域も小さくなる。

この変種として、文字ではなく行を処理の単位にする方式もある。

(行指向方式)
while (入力がある) {
    一行読み込む;
    一行分のデータに対して処理を行う;
    結果を出力する;
}

実際には、ほとんどの場合、処理(と出力)の部分は以下のようにパターンとの照合でガードされている。

(行指向パターンマッチ方式)
while (入力がある) {
    一行読み込む;
    if (あるパターンに一致する) {
        一行分のデータに対して処理を行う;
        結果を出力する;
    } else if (別のパターンに一致する {
        別の処理を行う;
        結果を出力する;
    } ...
}

ここまで来ると、フィルタを記述するのに必要なのは、処理が必要か否かをガードするパターンとパターンに応じた処理の記述(アクション)だけだということがわかる。残りの部分はフィルタの種類によらず共通なのだ。文字や行のような処理の単位も区切りを指定することで柔軟に変えることができる。AWK はこの発想を実現するために作られたプログラミング言語だ。

Scheme/Lisp らしく書くには

Ruby や Python なら、先の「行指向パターンマッチ方式」を素直にほぼそのままの形で実装できる。一方、Scheme/Lisp ではデータの基本構造はリストで、繰り返し処理は再帰で実現する。リストと再帰で「行指向パターンマッチ方式」を実現するのは難しい。その代わりと言っては何だが、最初に挙げた「汎用方式」をすっきりと実現できる。すなわち、以下のようにする。

  1. 入力を単位(たとえば行)ごとに並べてリストを作り
  2. 各要素に対してフィルタ関数を適用し、結果を並べたリストを作り
  3. 結果のリストを出力する

リストから別のリストを作って出力するというのは、Lisp プログラムの基本パターンだ。前回の Gauche による実装も、(たどたどしい書き方ではあるが)この方式を実現したものになっている。

ただ、すでに述べたように、この「汎用方式」による実装は効率が良くない。ループのことはともかくとして、メモリのことは気になる。もう少し効率良く書くことができないだろうか?

まあ、実際問題として、今どきのコンピュータ(Mac や PC)で、ちょっとしたテキスト処理をするぐらいのことで、効率(時間やメモリ)のことを気にする必要もないんだけどね。

参考文献

Brian W.Kernighan, Rob Pike
アスキー ( 1985-09 )
ISBN: 9784871483513
おすすめ度:アマゾンおすすめ度

関連リンク

  • AWK (Wikipedia:ja)

関連記事

twitter より (2010-09-16)

  • 17:31  iPod touch 、出荷のお知らせキターーーーー。配送状況を確認したら海外荷物受付になってる(; ゜д゜)。上海から海を越えて来るらしい。週末には届かんな...orz
  • 17:38  iMac のときは(これも上海からやってきた)、7/30 に出荷のお知らせが来て物が届いたのは 8/2。今度も同じ日数で届くとすると日曜(9/19)。ま、月曜には届くか。
  • 23:57  むむ。当たってる。なんでわかるんだろう(・д・)?→ mnbi『え、ストレス?感じるほどの環境に居ないから分からない』 問題無し http://shindanmaker.com/49492
Powered by twtr2src.

2010-09-16

正規表現を使った処理を scheme (Gauche) で書いてみる

今日は時間がないので、とりあえず、コードだけを貼っておく。これで、前回の Ruby、Python のコードと同じ結果になる。

試行錯誤の繰り返しの果てにでっちあげたものだから、もっと手を入れたかったが、あと少しで今日が終わるのでもう時間切れだ。また明日、考えることにする。Scheme/Lisp らしい書き方とか、Ruby で書いたプログラムの方を Scheme に移植しやすく直してみる、とか。

参考文献

プログラミングGauche
Kahuaプロジェクト
オライリージャパン ( 2008-03-14 )
ISBN: 9784873113487
おすすめ度:アマゾンおすすめ度

関連リンク

関連記事