io-streams
そろそろio-streamsで遊んでみよう。いや遊んでたのは実のところ随分前なんですけど。
http://hackage.haskell.org/package/io-streams
io-streamsというのはHaskellのストリーム処理ライブラリの一つで、シンプルなのが売りなのかな。
ストリーム処理には私はよくconduitを使っていますけど、conduitと比べるとなんというか、本当にシンプルで使い勝手がいいです。
まずはHelloWorld的に、猫のようなもの。
import System.IO.Streams main :: IO () main = stdin `connect` stdout
ここで出てくるstdinとstdoutはSystem.IO.Streams.Handleに定義されてるもので、それぞれこんな感じになってる。
stdin :: InputStream ByteString
stdout :: OutputStream ByteString
で、connectはSystem.IO.Streamsに定義されてて、こんな感じ。
connect :: InputStream a -> OputputStream a -> IO ()
標準入力からの入力ストリーム(InputStream)を標準出力への出力ストリーム(OutputStream)に接続(connect)しているというだけ。
System.IO.Streams.HandleにあるhandleToInputStream/handleToOutputStreamを使って書き直すとこんな感じ。
import System.IO (stdin, stdout) import System.IO.Streams hiding (stdin, stdout) main :: IO () main = do is <- handleToInputStream stdin os <- handleToOutputStream stdout is `connect` os
ついでにHandleで書くとこんな感じ。
import System.IO main :: IO () main = do c <- hGetChar stdin hPutChar stdout c main
HandleToStreamと同じようにStreamToHandle系の関数もあるのでなんとなくストリームとIOハンドルは同じものなんだなぁというのがわかりますね。どうでもいいですけど。
ファイルから読むのはwithFileAsInput/withFileAsOutputというのがあって、withFileと同じような雰囲気で使えます。
% cat data.txt abc def ghc
みたいなファイルがあったとして
import System.IO.Streams main :: IO () main = withFileAsInput "data.txt" $ \is -> is `connect` stdout -- > "abc\ndef\nghc\n"
ファイルの中身を吐き出すだけのプログラムができる。中身はおそらくwithFileとhandleToInputStreamを組み合わせただけであろう。
System.IO.Streams.Listにはストリームをリストにしたりリストをストリームにしたりする処理が色々入っていて、おそらくよく使うのはtoListなんじゃないかなぁ。
import System.IO.Streams main :: IO () main = withFileAsInput "data.txt" $ \is -> do as <- toList is print as -- > ["abc\ndef\nghc\n"]
なんかあんまし嬉しくなかった。ストリームの一番ベースとなるデータ構造ByteStringには区切りが無いのでまあこうなるよね。
System.IO.Streams.ByteStringにはlinesという改行ごとにByteStringを区切る関数があって、まあData.List.linesとだいたい同じように動きます。
import Prelude hiding (lines) import System.IO.Streams main :: IO () main = withFileAsInput "data.txt" $ \is -> do ls <- lines is as <- toList ls print as -- > ["abc", "def", "ghc", ""]
最後改行で終わってるからなんかゴミ入ってますけど、これで意図通り。isはただのバイト列のストリームですが、lsはバイト列のリストのストリームになっています。わかりづらいですが。
InputStream/OutputStreamはMonadじゃないけど、linesとかtoListとかがIOなのでdoで連結できるし、printとかとも簡単に連携できて良い感じです。それはそうと今MBAのキーボードに水をこぼしてAのキーがすごくききづらくなりました。
bindで書くとちょっといい感じに。
main = withFileAsInput "data.txt" $ lines >=> toList >=> print -- > ["abc", "def", "ghc", ""]
別にconnectしなくてもデータ取り出せるのがなんかお手軽な気がします。ここではtoListがconnect相当をやってるんですけどね。
toList :: InputStream a -> IO [a]
モナドトランスフォーマーとか使わないでいいのが良いと思います。
InputStreamにはread/unRead/peekがある。conduitでいうところのawait/leftoverみたいなものです。conduitにpeek相当ってあったっけ?
{-# LANGUAGE OverloadedStrings #-} import Prelude hiding (lines, read) import Data.ByteString.Char8 () import System.IO.Streams main :: IO () main = withFileAsInput "data.txt" $ \is -> do ls <- lines is a1 <- read ls print a1 -- > Just "abc" unRead "xyz" ls a2 <- peek ls print a2 -- > Just "xyz" as <- toList ls print as -- > ["xyz", "def", "ghc", ""] a3 <- read ls print a3 -- > Nothing
readはストリームを消費するけどpeekは消費しない。unReadは本来は一度読んだデータを読まなかった事にする関数だけど、型が合ってれば別になんでも詰め直せる。で、toListはストリームを全部消費するのでその後に何か読もうとしても読めない。
一方、writeはデータをOutputStreamに書き出すだけです。conduitのyieldとは全然違う。
あと/dev/nullみたいなnullInput/nullOutputみたいなのもあります。nullOutputはまあいいとして、nullInputはunReadと組み合わせてスタックみたいに使えそうですね。いやそんなのリスト使えって感じですね。
データ変換、conduitでいうところのConduit相当のものは、単に
f :: InputStream a -> InputStream b
みたいな関数を作ってやればいいだけみたいなんですが、えっとこれどうやって作るんだ。みたいになりますが、おそらくそういう時のためにGeneratorというのがあります。GeneratorはInputStreamを作るための機能のようですが、こいつはMonadでMonadIOなのでInputStreamの変換にも使いやすそうです。おい、結局モナドトランスフォーマー使ってるじゃないか。
ためしに
data Member = Member ByteString deriving (Show) f :: InputStream ByteString -> IO (InputStream Member)
みたいな関数fを作ってみるとすると、
{-# LANGUAGE OverloadedStrings #-} import Control.Monad.IO.Class (liftIO) import Data.ByteString (ByteString) import Data.ByteString.Char8 () import Prelude hiding (lines, read) import System.IO.Streams data Member = Member ByteString deriving (Show) f :: InputStream ByteString -> IO (InputStream Member) f = fromGenerator . g g :: InputStream ByteString -> Generator Member () g is = do ma <- liftIO $ read is case ma of Nothing -> return () Just a -> do yield (Member a) g is main :: IO () main = withFileAsInput "data.txt" $ \is -> do ls <- lines is ms <- f ls toList ms >>= print -- > [Member "abc", Member "def", Member "ghc", Member ""]
gなんて関数が増えちゃいましたけど、だいたいこんな感じ? Generator部分(g関数)はあんましConduitと変わりませんね。一旦Generatorを返す関数を作らないと、データの繰り返し処理ができなくなる。というのもだいたいConduitと同じ。
ということでio-stream結構いいんじゃないでしょうか。
周辺ライブラリの充実度でconduitに見劣りするのもの、インタフェースが簡素で他との連携もやりやすいので、頑張ればなんとかなるんじゃないでしょうか。Networkとかattoparsecなんかは最初から入ってるから、頑張って作ろう。
とここまで書いたところでチュートリアルの存在に気づきました。Tutorialってモジュールがある……。
System.IO.Streams.Tutorial
今回作ったソースはここです。
exercises/streams at master · yunomu/exercises · GitHub