そろそろ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
ファイルの中身を吐き出すだけのプログラムができる。中身はおそらくwithFileとhandleToInputStreamを組み合わせただけであろう。
System.IO.Streams.Listにはストリームをリストにしたりリストをストリームにしたりする処理が色々入っていて、おそらくよく使うのはtoListなんじゃないかなぁ。
import System.IO.Streams
main :: IO ()
main = withFileAsInput "data.txt" $ \is -> do
as <- toList is
print as
なんかあんまし嬉しくなかった。ストリームの一番ベースとなるデータ構造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
最後改行で終わってるからなんかゴミ入ってますけど、これで意図通り。isはただのバイト列のストリームですが、lsはバイト列のリストのストリームになっています。わかりづらいですが。
InputStream/OutputStreamはMonadじゃないけど、linesとかtoListとかがIOなのでdoで連結できるし、printとかとも簡単に連携できて良い感じです。それはそうと今MBAのキーボードに水をこぼしてAのキーがすごくききづらくなりました。
bindで書くとちょっといい感じに。
main = withFileAsInput "data.txt" $ lines >=> toList >=> print
別に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
unRead "xyz" ls
a2 <- peek ls
print a2
as <- toList ls
print as
a3 <- read ls
print a3
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
gなんて関数が増えちゃいましたけど、だいたいこんな感じ? Generator部分(g関数)はあんましConduitと変わりませんね。一旦Generatorを返す関数を作らないと、データの繰り返し処理ができなくなる。というのもだいたいConduitと同じ。
ということでio-stream結構いいんじゃないでしょうか。
周辺ライブラリの充実度でconduitに見劣りするのもの、インタフェースが簡素で他との連携もやりやすいので、頑張ればなんとかなるんじゃないでしょうか。Networkとかattoparsecなんかは最初から入ってるから、頑張って作ろう。
とここまで書いたところでチュートリアルの存在に気づきました。Tutorialってモジュールがある……。
System.IO.Streams.Tutorial
今回作ったソースはここです。
exercises/streams at master · yunomu/exercises · GitHub