ストリーム (プログラミング)

ストリーム: stream)とは、データを、比較的小さい単位が連続したものと捉え、上流から下流へ「流れるもの」とみなし、そのデータの入出力・送受信(途中段階を含む)を最小限の滞留とさせ低遅延処理となるように扱う形態を指す。またその操作のための抽象データ型を指す[1]

処理内部では適切なデータ分割・バッファリングが行われる。

入力ストリーム (input stream) を利用してデータの読み出しを行ない、出力ストリーム (output stream) を利用してデータの書き込みを行なう。対照的な概念としては、保管・永続化されたデータ形態(ファイルデータベースなど)がある。

メモリバッファの入出力を扱うもの、ネットワーク通信を扱うものなどさまざまなものがある。ファイルの入出力(読み書き)に対しては、ストリームとの変換を行う仕組みが用意される。

典型例として、Unixオペレーティングシステムおよびそのシェルによって実装されたものがある(標準ストリームパイプラインリダイレクト[2]

標準ストリーム

オペレーティングシステム(例えばUnix)の標準的な入力元や出力先に対して、標準ストリームと呼ばれる特別なストリームが用意される。

キーボードからの入力、ディスプレイへの出力も、標準ストリームによって抽象化され、個別のプログラムからはデバイスハードウェアを意識する必要がなくなる。

C言語

C言語ではUnix由来の標準ストリームとして、標準入力stdin、標準出力stdout、標準エラー出力stderrが定義されている。これらはすべてファイルストリームとして抽象化されており、ファイルポインタFILE*のひとつとして扱うことができる。標準Cライブラリヘッダーファイル<stdio.h>において、ストリームを扱う関数が定義されている。例えばprintf関数は標準出力への書き込みを、scanf関数は標準入力からの読み出しを行なう。

C++

入出力ストリーム

C++では、標準C++ライブラリのヘッダーファイル<iostream>において、標準入出力ストリームとしてcincoutcerrclogが定義されている。ワイド文字列用には別途wcinwcoutwcerrwclogが定義されている。

#include <iostream>
int main()
{
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

C++は強く型付けされた言語であるが、この標準入出力ストリームは多重定義によって全く型を意識せずに入出力を行なえる。特定の演算子を多重定義したユーザー定義型(クラスあるいは構造体)も利用できるため、非常に柔軟な入出力が可能である。さらに入出力操作子(マニピュレータ)の導入により、細かな制御が可能となった。

#include <iostream>
#include <iomanip>
...
int i;
std::string s;
my_data_type d;
std::cin >> i; // 整数の入力をiに読み込む。
std::cin >> std::setw(10) >> s; // 10文字読み込む。
std::cin >> d; // オーバーロードすることで任意のデータをdに読み込む。

これらの機能により、従来の型消去 (type erasure) や型昇格 (type promotion) を伴う可変長引数を利用したprintf関数で起こりがちであった、型の不一致や制御子の過不足によるバグの可能性がきわめて低くなった。しかしその半面、マニピュレータの仕様など覚えなければならないことが増加し、また記述量もprintfと比べ格段に増えてしまったため、敬遠するプログラマも多い。

文字列ストリーム

標準ヘッダーのひとつ<sstream>には、文字列を対象としたストリーム操作の機能を提供するクラスistringstreamostringstreamが定義されている。

外部ライブラリ

Boost C++ライブラリにおけるboost::format型安全sprintf()であり、出力の際のマニピュレータの記述の煩雑さを軽減する。

Javascript

ストリーム API[3]を持つ(WHATWGにより策定が進められている)。

Node.js

ストリームを扱うモジュールを持つ。

Java

ストリーム API を持つ。

.NET Framework

.NET Frameworkでも入出力をストリームに一般化しており、ここでは低水準なRead, Writeメソッドのみサポートしている。以下に代表的なクラスを挙げる。

System.IO.Stream
一般的なストリームのインターフェイスを提供する抽象クラス
System.IO.FileStream
ファイル入出力機能を提供するストリーム。
System.IO.MemoryStream
メモリバッファの入出力機能を提供するストリーム。
System.Net.Sockets.NetworkStream
ネットワークの入出力機能を提供するストリーム。

上記のストリームを読み書きするためのクラスが別途存在し、通常はこれらを用いて入出力処理を行なう。

System.IO.BinaryWriter / System.IO.BinaryReader
ストリームに対してバイナリ形式での読み書き機能を提供するクラス。
System.IO.TextWriter / System.IO.TextReader
ストリームに対してテキスト形式での読み書き機能を提供するクラス。
System.IO.BufferedStream
既存のストリームにバッファリング機能を追加するためのストリーム。

関数型言語

関数型言語においては専ら、無限の大きさ(長さ、要素数)の再帰的なデータ構造を指す。遅延評価を用いて実装されるため「遅延ストリーム」とも呼ばれる。無限のデータを扱うには、そのうちの一部を切り出したりする関数も必要なほか、ストリームを成すデータも再帰関数によって生成されるため、オブジェクト指向の関数型言語では、これらをまとめてストリームクラスとして実装することもある。関数が出力するストリーム自体は連結リストとして実装されることが多い。

遅延評価は必要なときに必要なだけ関数を評価し、不要になったら関数の評価を正常に中断することができる。このことは再帰呼び出しを中断する条件(停止条件)の判定を一切行わない再帰関数や再帰的な値の定義を可能にする。たとえば階乗を求める関数は、1から順に再帰的にかけ算してその結果を返すよう定義するだけである。上限などを設ける必要はなく、かわりに「結果をn個求めてリストにする」関数や「結果をx個捨てる」関数などを介して呼び出す。条件判定をしないということは、遅延評価を行わない言語では無限ループに陥いるということである。しかしそのような言語でも無限の要素数を持つデータを扱えるようにするため、ストリームに関してだけ遅延評価を取り入れている。

無限に再帰する関数や値から一部を取り出したりするしくみは巧妙にできている。「結果をn回求める」関数をrepeat、再帰関数をrecとしてその経過を見る。

  1. repeatがrecを1度だけ呼び出す
  2. recは再帰する直前の状態で一旦結果を返す
  3. repeatは結果とともに、recを1回分だけ評価された関数(サンク。クロージャの一種)として受け取りリストに連結する
  4. recを呼び出した回数がnでないなら、recをサンクの状態からフォース(強制評価)する
  5. 2から4を繰り返す
  6. recの結果を返す

サンクは関数を評価した途中経過と見做せる。recを階乗関数factとして3回評価する場合のサンクの状態を(概念的にだが)示す。 fact関数は fact i = i * (fact i+1) と定義すればよい。

  1. repeat 3 (fact 1)
  2. repeat 3 (1 * fact 1+1)
  3. repeat 3 (1 * 2 * fact 2+1)
  4. repeat 3 (1 * 2 * 3 * fact 3+1)
  5. ---> 6

カッコの中全体がサンクと言える。また、フォースは「カッコの中と関数をひとつの関数として呼び出すこと」と言える。

遅延評価が標準である言語ではすべての再帰的な定義がストリームのように機能するため、とくにストリームと呼ぶことはない。しかしHaskellにおいては、モナドが導入される以前は、参照透過な入出力を実現するためのデータ構造をストリームと呼んでいた。

脚注

  1. ^ ストリームとは - IT用語辞典 e-Words
  2. ^ ただしxargsコマンドで-Pオプションを指定し並列処理させた場合の実行コマンドの標準出力はバッファリングが働かず混合が生じる。
  3. ^ 参考: https://triple-underscore.github.io/Streams-ja.html

関連項目