Skip to content
SATO Mitsuhide edited this page Jan 4, 2016 · 5 revisions

Home


1.pipeとは

pipeとは、プログラミング言語perfumeで利用できるコルーチン間で入出力をパイプラインのように受け渡すためのユーティリティです。
バージョン0.8.0から利用可能となりました。

2.UNIXのパイプとの比較

UNIXのパイプは、あるプロセスの出力と別のプロセスの入力をつなげるためのOSに用意された仕組みです。
たとえば、あるファイルに書かれた特定の文字列が含まれる行数を数えるのに、UNIXではよくこんな書き方をします。

% grep 'foo' FILE | wc -l

ここで、grepはFILEを読み込み、行中に'foo'というパターンを見つけた場合、その行を標準出力へ出力します。
そして、wcは標準入力からファイルを入力し、その行数を数えた結果を出力します。

この例の動作そのものを、perfumeの処理系の中で実現したのが一連のpipeユーティリティです。

3.perumeのパイプ

UNIXのパイプの例でgrepやwcのような構成要素は、perfumeの場合はperfumeのステートメント(文)となります。
そして、標準入力および標準出力に相当するものは、perfumeのグローバル変数に定義された$stdinと$stdoutになります。
次の関数は、pipeユーティリティ上で動作します。

defun cat () {
    $stdin each do: {| r |
        println $r;
    };
};

これは、単純に標準入力から読み込んだデータを標準出力へ出力する関数です。
また、関数ではなく、以下のステートメントでも問題ありません。

println [read];

上の例では、readは$stdinから入力し、その値をprintlnにより$stdoutに出力するため、pipeの対象となります。

各ブロックは独立したコルーチンとして実行されます。これは、$stdin と $stdout を各ブロック間で多重化するためと 各ブロック間のFIFOキュー(実態は Streamクラスのオブジェクト)の溢れ制御を行うために必要なことです。

4.pipeの実行方法

それでは、最初のUNIXの例のように特定の文字列にマッチした行数を出力するためのpipeの使い方を見てみます。
用意するのは、grep関数とファイルの行数を数えるwc関数です。
それぞれの関数定義は以下のようになります。

grep

defun grep (pat) {
    set n 1;
    $stdin each do: {| r |
        if {$r =~ $pat} then: {
            println $INFILE ":" $n ": " $r;
        };
        $n ++;
    };
};

wc

defun wc () {
    set i 0;
    $stdin each do: {
        $i ++;
    };
    println ["%6d: %v" fmt $i $INFILE];
};

これをpipeコマンドでつなげて見ます。以下のようにプログラムします。

pipe in: "FILE" {grep 'foo'} {wc};

ここで "FILE" は入力するファイル、'foo' はマッチする文字列パターンです。
最初にみたUNIXの例と比較してみます。

% grep 'foo' FILE | wc -l

UNIXの場合は | 文字により各コマンドをつなげます。perfumeのpipeの場合は、まず関数pipeの呼び出しがあり、パイプとして呼び出す文を'{' と '}'で囲みます。 そして、データは '{' と '}' で囲まれたブロックの左から右に流れます。
また、UNIXの場合は、コマンドにファイル名を与えますが、perfumeのpipeの場合はpipeコマンドのin:オプションで指定します。 指定しない場合は標準出力からの入力となります。
もちろんパイプしたい関数の中の引数にファイル名を指定することもできますが、汎用的にパイプのコマンドを作りたい場合は、常に$stdinから読み込ませる作りの方が便利でしょう。

pipeを使って、単純にファイルの先頭行だけ出力したい場合は以下のようにすればよいでしょう。

pipe in: "FILE" {println [read]};

5.複数ファイルの取り扱い

pipeを複数のファイルに対して適用したい場合もあります。
その場合、in: オプションに対してリストでファイル名を指定します。

pipe in: ("FILE1" "FILE2") {grep 'foo'} {wc};

こうすることで、”FILE1" と ”FILE2” に対して再帰的にpipeが適用されます。

また、glob ユーティリティを用意しましたので、正規表現でファイルを選択することができます。 たとえば、C言語のソースコード(*.c と *.h)の行数を数えるためには以下のようにすれば良いでしょう。

pipe in: [glob '\.[ch]$'] {grep 'foo'} {wc};

(注) globはUNIXのglob(3)とは異なり、正規表現でファイルのマッチングを行います。上の例だと、'*.[ch]' とは書けません。

6.pipeの入れ子

上の例のC言語のファイルの行数を数える例だと、複数指定した各ファイルについてそれぞれの行数がわかりますが、すべての合計についてはわかりません。
そのような場合は、以下の例のようにpipeを入れ子にすることで求めたい値を得られます。

pipe {pipe in: [glob '\.[ch]$'] {grep 'foo'}} {wc};

7.ファイルへの出力とストリームへの出力

pipeの実行結果をファイルに出力したい場合、pipeのオプション out: に出力先のファイルを指定します。
以下のようになります。

pipe in: "INFILE" out: "OUTFILE" {grep 'foo'} {wc};

また、出力結果を中間的にメモリ上に蓄えたいこともあります。その際、Streamオブジェクトを使用します。
以下の例では、オブジェクト $x に値が蓄えられます。

x ::= new Stream;
pipe in: "INFILE" out: $x {grep 'foo'} {wc};

この$xは、別のpipeの実行の際に入力として与えることもできます。

$x close;
pipe in: $x {cat};

8.ファイル名の参照

パイプを実行する各ブロックの中で、現在読み込んでいるファイル名を参照したい事があります。
たとえば、grep の出力結果としてファイル名を出力したい場合などです。 その際は、各ブロックに定義されるグローバル変数である $INFILE を参照します。
先ほどのgrep関数の定義を見てみます。$INFILE が参照されている事がわかります。

defun grep (pat) {
    set n 1;
    $stdin each do: {| r |
        if {$r =~ $pat} then: {
            println $INFILE ":" $n ": " $r;
        };
        $n ++;
    };
};

出力ファイル名についても同様に、$OUTFILE で参照できます。

$INFILE、$OUTFILE がそれぞれ標準入力、標準出力を参照しているときは、"-" が設定されます。

9.オブジェクトの受け渡し

通常の$stdinや$stdoutは文字列データを入出力しますが、pipeを構成するブロック間ではperfumeで利用可能なすべてのデータ型を受け渡しすることができます。
前に説明した Stream オブジェクトを指定することで、パイプの結果として任意のデータ型の Stream を得る事が可能です。
以下の例では、整数型のオブジェクト(1)を生成しパイプで次のブロックに渡し、整数演算を行っています。
結果は Stream オブジェクトである $x に蓄積されます。

x ::= new Stream;
pipe out: $x {$stdout puts 1} {$stdout puts [[$stdin gets] + 1]};

注意事項として、文字列以外のオブジェクトを受け渡す際は、println を使用せず、$stdout puts を使用することです。println は受け取ったパラメータをすべて文字列に変換するためです。

Clone this wiki locally