yunomuのブログ

プログラミング関係

Chrome remote-debuggingの話と見せかけてWebSocketとかfrappuccinoとか

最近ちょっとブラウザで遊んでいたので、共有というか。

Google Chromeにremote debuggingという機能があります。
https://developers.google.com/chrome-developer-tools/docs/debugger-protocol

要するにJavaScript開発者にはおなじみのDeveloper toolsが使っているAPIなんですが、最近モバイル対応だのなんだののために整備されて使いやすくなったとかなんとか。最近て、何年前の話か知りませんけども。

使うためには、remote debugging portを有効化してChromeのプロセスを起動する必要があります。Chromeのプログラム名は環境によって違うので、それらしいやつを起動してください。

% google-chrome --remote-debugging-port=9222 --user-data-dir=/tmp

とやると起動します。9222はなんか、デフォルトのポートらしいです。
user-data-dirはプロファイルのディレクトリで、指定しないと既存のChromeプロセスがforkするだけでremote-debuggingは有効になりません。既存のプロセスが無ければうまくいくんですが。

起動すると、
http://localhost:9222/
にアクセスできるようになります。このトップページは現在開いているタブの一覧です。それぞれのページを開くと、おなじみのdeveloper toolsが開きます。developer toolsってJavaScriptで書かれてたのかよという。

http://localhost:9222/json
にアクセスすると、開いているタブの情報がJSONで取れます。
この中にある”webSocketDebuggerUrl”というやつが、remote-debugging APIのエンドポイントです。その名の通り、WebSocketで通信します。

ここからは手打ちでは辛いのでRubyで操作します。
まず、どうでもいいんですがwebSocketDebbuggerUrlの取得部分。

require 'json'
require 'open-uri'

# ページ一覧のJSONを取ってくる
def pagelist(host = "localhost", port = 9222)
    JSON.parse open("http://#{host}:#{port}/json") {|f|
        f.read
    }   
end

# とってきたやつからタイトルに”GitHub”が含まれる最初のものを探す
w = pagelist.find {|e| e["title"].include? “GitHub }

# webSocketDebuggerUrlを取得
debugUrl = w["webSocketDebuggerUrl"] || begin
    puts "debugUrl is nil"
    exit 1
end

ここではタイトルに”GitHub”を含む最初のタブのdebugger urlを取得していいます。これとは別にdeveloper toolsを使っていたりするとこのインタフェースが無くなったりするので、念のためにエラー処理を入れてます。

で、WebSocketでの通信ではwebsocket-client-simpleを使います。たぶんこれが一番楽です。

% gem install websocket-client-simple
# (さっきの続き)
require 'websocket-client-simple'

# 接続
ws = WebSocket::Client::Simple.connect debugUrl

# 以下はイベントハンドラ

ws.on :message do |msg|
    data = JSON.parse(msg.data)
    print "response: ", data["params"], $/
end

ws.on :open do
    puts "open"
end

ws.on :close do |s|
    puts "close"
end

基本はこんな感じ。イベントドリブンになっていて、message, open, closeというイベントが起きます。messageは受信した時、openは接続完了した時、closeは閉じた時。
それとは別に、sendというメソッドがあります。

試しにTimelineで流れてくるイベントを取ってみます。
https://developers.google.com/chrome-developer-tools/docs/protocol/1.1/timeline
ここに載ってるstartというメッセージを送ると、ブラウザ側からイベントのログがドロドロと流れてきます。
WebSocketのopenのタイミングが微妙なのでopenハンドラに直に送信処理を書きます。

ws.on :open do
    ws.send JSON.dump("id" => 1, "method" => "Timeline.start")
end
sleep 5

こうすると5秒ほどイベントが流れてきます。流れてこないという人はJavaScriptイベントが起きていない可能性があるのでブラウザのページ上でマウスを動かしたりなんたりしてみてください。
あと、このidというのは、まあなんかidらしいです。適当に決めてます。

{
  "record"=>{
    "startTime"=>1388377232581.642,
    "type"=>"Program",
    "data"=>{},
    "children"=>[],
    "endTime"=>1388377232581.6663
  }
}

イベントの1個の形式がこんな感じで(Rubyに整形済み)、いわゆるTimelineの情報が入っているんですが、このstartTime/endTimeというやつが、エポックからのまさかのマイクロ秒で、ミリ秒じゃねーのかよってつまんないところでつまづきました。

frappuccino

messageイベントのハンドラを書くのが面倒くさい。ので、frappuccinoでイベントをストリームに変換してみようと思いました。いわゆるFRPライブラリです。
https://github.com/steveklabnik/frappuccino

% gem install frappuccino
require 'frappuccino'

# イベントストリームを作るやつ
class Emitter
  def send(data)
    emit(data)
  end 
end
emitter = Emitter.new
stream = Frappuccino::Stream.new(emitter)

# debug: 送られてきたイベントを画面に出力する
#stream.on_value do |v|
#  puts v
#end

# eventRecordedだったら画面に出力する
stream
  .select {|d| d["method"] == "Timeline.eventRecorded"}
  .on_value {|d| puts d } 

# eventRecordedじゃなかったら"log"というファイルに書き出す
stream
  .select {|d| d["method"] != "Timeline.eventRecorded"}
  .on_value {|d| open("log", "a+") {|f| f.puts d } } 

# (中略: 最初に書いたWebSocketを接続したりする処理)

# データを受け取ったらストリームに流す
ws.on :message do |msg|
  emitter.send JSON.parse(msg.data)
end

これはわかりやすくなったんだろうか?
でもまあ、今回みたいにselectが複数あったりとか、あと畳み込みをしたくなった時には結構いいかもしれません。この例だとzipやmergeはあんまし使わないかなぁ。

しかしストリームをイベントに変換してそれをまたストリームに変換とういのもなんか妙な話ですね。

ノートの使い方

ノートの話をたまに書いているので、じゃあ次はペンの話でも書こうかと思ったけど、ノートの使い方の話は書いてないよねと指摘を受けたので書いてみます。そういやそうだよね。断片的には書いてるけど。

まず基本として、ノートは記憶の補助として使っています。というかそれ以外で何かあるのかな。
その、記憶の中でもかなり短期、1日とか数日とか、おおむね1週間以内の情報を置くために使っています。読み返すとしても数ページ、多くても10ページとかそういう感じで、それ以上前の事はあまり覚えてもいません。それと関係あるかは微妙ですが字は雑です。

それと、1ページの中で日付をまたぐ事もめったにしないので、1〜2行で終わってたり、タイトルだけだったり、ヘタすると日付だけだったりもします。
まあもったいない使い方のような気もしないでもないですけど、余白も情報だと思いますし、詰めて書いて追記できないのもバカバカしいし、「もったいないと思うならリサイクルに出せばいい」と解決になってるんだかなってないんだかわかんない理屈もあります。単に何も気にしていないというだけですが。

書いている内容は、リストです。テーマごとに項目を列挙しているだけです。そのテーマというのが日記(と呼んでる今日の仕事)だったり開発の検討項目だったり議事録だったり作業ログだったりする。で、だいたい1日に数ページ進む。
これを、今自分が何をやっているかを思い出すために使っています。あー議事録だけは普通のメモですけども。

基本的にはスタックというか、気が散ったり別の仕事をしたり飯を食ったり帰って寝たりした後でも作業を迅速に再開できるように残している感じです。なのでまあ、数日前の内容にあまり意味はありませんし、割り込みが無い時や無視していい時、あと割り込まれても復帰が簡単な時はそもそもノートを使ったりしません。
実際就職してから去年くらいまではあんまし使ってなかった。逆に大学でソースコード読みとかしてた頃は数日で1冊とかのペースで消費してる時もありました。call traceとか一瞬目を離したら忘れるし。

あとは、暇つぶしだったりやる気無い時の仕事してるふりだったり、まあそれにしてもリストですね。やりたくない事リストを書いて渋々覚悟を決めたり、やらなくていい理由を考えたり。

それ以外の情報は、予定や締め切りはGoogleカレンダーや会社のグループウェアに書きますし、もう少し広いプロジェクトや進捗状況や議事のまとめは別の資料に書きますし、検討項目も別の資料やプロジェクトのtracとかRedmineとかに書きますし、思いつきはGithubのissueとかTwitterとか、ブログのネタは溜まるとストレスになるから書かないで忘れるに任せる。いや書きかけのテキストファイル結構ありますけども。

というわけで、本当に今やってることや今からやることや今考えたことを書いているだけです。
なのであまり職場以外で持ち歩く必要がなくて、ノートの記事とかで忙しくなったら小さいノートがほしいとか言ってるのは外で仕事の事を考える必要が出た時の事を考えていました。今のところ職場にいる時間を使えば済む程度だからまあいいかなぁと。
あと、こういう使い方なので持ち歩くにしても切り替え時期に2冊持つくらいで済むので、カバンにやさしい。

ノートっていうのは非常に自由度が高い道具なので使い方はそれこそ千差万別で、私とは逆に長期の記憶として使ったり、メモ帳だったり、スケジュール帳だったり、プロジェクト管理だったり、いや後半はノートっていうより手帳かな。違いはよくわかりませんが。
長期の記憶として使う場合は鉛筆で下書きをしたり、下書き用のノートを別に持ってたりするらしいですよ。重要な事は何回書いてもいい。気持ちはわからんでもないけど私は面倒くさい。

ノートなんぞ使わなくていい生活がしたいものです。

アルゴリズム問題

最近、CodeIQというサイトの「今週のアルゴリズム」を解くのが楽しい。元々私はこういうアルゴリズム問題を解くのが非常に苦手なんですが、まあこの「今週のアルゴリズム」の問題は簡単だし。20分とはいえないかもしれないけど1時間もかかんない感じだし。

で、この問題
第2回「今週のアルゴリズム:日付の2進数変換」解説 #ruby|CodeIQ MAGAZINE
日付を例えば2013年12月7日なら20131207という整数値にして、それを2進数に変換して、そのビット列が左右対称になるような日付を、1964年10月10日から2020年7月24日までの間で全て列挙しろ、と。

解き方はおおまかに2種類あって、
(案1) 1964/10/10から日付を加算しながら、数値や2進数に変換して対称の判定をする。
(案2) 左右対称なビット列を列挙して、それぞれが日付として解釈できるかを判定する。

案1の方はまあ簡単です。ああでもHaskellで書いたんで、10進2進変換を自前で書かなきゃいけないのが面倒だった。「日付+1日」というのも無くて、これは+1すればいいだけなので簡単だったんだけど、Rubyにすりゃよかったとちょっと思った。
探索空間は60年分くらいあるのでちょっとアレだが、1年400日としても2万4千個くらいになると見積もった。

案2の方は、左右対称なビット列を作るのが若干面倒だし、Haskellだと文字列を日付として正しいかどうかを判断するのが結構面倒(Data.Time.parseTimeが19656617を平然と1965年12月31日と読んだり。Rubyにすりゃよかった)なので採用しなかったんですが、解説によるとこっちの方が圧倒的に速いらしい。
私の感覚的には10進8桁は30ビット弱くらいだから、左右対称なので半分の15ビット弱くらいになって、まあ3万もいかないだろうけどもどっこいどっこいかなぁと思ったんですが。

本当に簡単だったので案1で5分くらいで書いたと思う。

フィードバックのメールを見ると、アルゴリズム問題なのでできるだけ速い案2で解いて欲しかったと書いてあって、そこまではまあそりゃそうだなんですが、模範解答で「19641010から20200724までは2進数にすると25桁で、12桁と真ん中1桁、12桁も全て1001から始まるので8桁+真ん中1桁の合計9桁、512個の数字を調べればいい」というようなことが書いてあった。
いや、そういうの考えるのが面倒だからコンピュータを使っているんじゃないのか。それにそんなこと言ってたらどこをとっても再利用できないプログラムができてしまうんじゃないか。一般化されてないコードを書くのは苦痛じゃないのか。
と思ったけどもよく考えたら問題文にプログラミングで解決しろとは書いてなかったのでどっちでもよかった。実際過去問見たら手計算で解いてる人もいるみたいだし。

ということでこれはただの自己主張なんですが、私は頭を使うのが面倒なのでコンピュータを使っています。あと、性能より一般化だ。遊びならなおさら。
私はアルゴリズムの高速化問題には向いてないかもしれない。

それはそうと、試しに案2の方も実装してみました。パーサを改造してちゃんとした日付しか読まないようにするのは簡単だったというかパース後の文字列比較で簡単にやった。左右対称なビット列を作るのは意外に面倒でした。小さい順に並べようとすると偶数桁の時と奇数桁の時で分けないといけないし、桁上りのタイミングを見ないといけないし。どうなんだろう。
でもそれらができれば簡単だし、速かった。いやちゃんと測ってないですけど、4倍くらい? 0から20200724までの対称なビット列の数は9027個しかなかった。これだけでも案1の日付数え上げの半分弱くらいで、19641010以上だと137個だから8890個は日付の判定までもいかないから、そりゃ速いわ。

誰か私の代わりにこういうの考える役をやってください。

これコードのネタバレいつまでダメなんでしょうね。

UTCTimeをEqで検査する

UTCTimeが関わるテストをQuickCheckで書こうとしてハマった。

まずUTCTimeをランダム生成するために、UTCTimeをArbitraryのインスタンスにしようとした。
UTCTimeはDayとDiffTimeの組になっていて、DayとDiffTimeはIntegerから作れるし、IntegerはArbitraryのインスタンスなので、UTCTimeのインスタンス化はこんな風に書ける。

instance Arbitrary Day where
    arbitrary = ModifiedJulianDay <$> arbitrary

instance Arbitrary DiffTime where
    arbitrary = secondsToDiffTime <$> arbitrary

instance Arbitrary UTCTime where
    arbitrary = UTCTime <$> arbitrary <*> arbitrary

まあ別にDayとDiffTimeまでインスタンス化する必要は必ずしも無いんですけど。

で、UTCTimeに対してTextとの間で相互に変換する関数があるとする。こういうの。

fromText :: Text -> Maybe UTCTime
toText :: UTCTime -> Text

fromTextは一応Maybeにしておきます。

これをQuickCheckでテストする。

main :: IO ()
main = hspec $ describe "test"
    prop "from/to text" prop_text

prop_text :: Text -> Bool
prop_text a = fromText (toText a) == Just a

このテストが通らないわけだ。
なぜか、prop_textの比較に使われているUTCTimeをshowで表示してみても全く同じなのに、(==)がFalseを返す。なんでや。

よく見るとDayとDiffTimeはIntegerから生成しているので、負の数の場合があり得る。
Dayの場合は0が1858-11-17になってるだけなのでいいんだけど、DiffTimeはUTCTimeの中では0-86400の間で、1日の中の経過秒数を表している。(最後の1秒はうるう秒らしい)
ただし、特にUTCTimeやDiffTimeを作る時にそういう値の制約がかかっているわけでもないので、0-86400の範囲外の整数値からも普通にDiffTimeやUTCTimeを作ることができてしまう。いやDiffTimeの時はいいんだけどUTCTimeを作る時は制約かけてくれよ。

その結果どうなるかというと、こういう感じになる。

Prelude Data.Time> UTCTime (ModifiedJulianDay 0) (secondsToDiffTime 0)
1858-11-17 00:00:00 UTC
Prelude Data.Time> UTCTime (ModifiedJulianDay 1) (secondsToDiffTime $ -86400)
1858-11-17 00:00:00 UTC

で、UTCTimeのEqのインスタンス化がこうなので

instance Eq UTCTime where
	(UTCTime da ta) == (UTCTime db tb) = (da == db) && (ta == tb)

まあ、showの結果が一致しててもこれじゃequalにならないよね。うん。
どうでもいいけどこれだったらderiving Eqでもよかったんじゃないかな。

ということなので、DiffTimeのArbitrary化はこうするとテストが通るようになる。

instance Arbitrary DiffTime where
    arbitrary = secondsToDiffTime <$> choose (0, 60*60*24 - 1)

"-1"は念のため。なくても一応動くんですけどね。

あと、実はDiffTimeは負の値にするとさっきの例のように日付が変わるんですが、増やすと日付が変わらずに時刻だけループする。けども1周しかループしない。

Prelude Data.Time> UTCTime (ModifiedJulianDay 0) (secondsToDiffTime 86400)
1858-11-17 23:59:60 UTC
Prelude Data.Time> UTCTime (ModifiedJulianDay 0) (secondsToDiffTime 86402)
1858-11-17 23:59:62 UTC
Prelude Data.Time> UTCTime (ModifiedJulianDay 0) (secondsToDiffTime 86500)
1858-11-17 23:59:160 UTC
Prelude Data.Time> UTCTime (ModifiedJulianDay 0) (secondsToDiffTime 172800)
1858-11-17 23:59:86460 UTC

なんというか、UTCTimeのデータにDiffTimeを使いまわさない方が良かったんじゃないかなぁ。

DiffTimeに大きな値を突っ込んでも一応UTCTimeは作れるんですが、テストで使うと当たり前ですけどparseでコケますね。

文具トーク

たまにキングジムとかコクヨのサイトを眺める趣味がある。
文具屋では当たり前に便利なものがそろっているんですが、こういうサイトだと新製品とかなんかよくわからないアイテムがあったりして、また違う面白さがあったりします。面白いだけで買ったりとかはしないんですが。

ちょっと面白いもの、「packetta」というやつ。
その場でデータ共有 ワイヤレス共有メモリ「パケッタ」| KINGJIM
PC等からiPhoneとかWiFi対応機器にデータを共有するやつ。アドホックなのかアクセスポイントなのかわかりませんけど、仕組みはわかりやすい。外でデータ授受が頻繁に発生する人には結構便利そうです。プライバシーやセキュリティの色々があるので企業間でそういうことはあまり無いかもしれませんけど。

あと「フィンガープレゼンター黒曜石」
フィンガープレゼンター│コクヨS&T
単に形と名前が面白いだけです。下の方に「コクヨ石(意志)=黒曜石」となんかレベルの高い事も書いてあります。レーザーポインタとの組み合わせとかBLACK OUTボタンとか、色々考えてるなぁと。あんまし広い会場で喋ったこと無いのでよくわかんない。使うの難しそうだ。

というような事をグダグダと想像しながら眺めているんですが、これはこれで記録しておくと面白いかもしれないですね。

ここから先はまたノートの話なんですが、ノートコーナーに測量野帳があった。
測量野帳ブライトカラー - コクヨS&T
測量野帳って、何故か最近ちょっと人気ある気がするんですが、気のせいですかね。普通に手帳として使ったり、日記として使ったりする人が結構いるとか。
そもそも測量する人が使うものなので、丈夫で持ち歩きやすくて外でも書き込みやすいというあたりが人気のようです。私はあまり外でノート使わないし保存もしないのでよくわかりませんけど、丈夫なのはいいですね。
PCで日記書くの面倒になったら使ってみよう。いや、既に結構面倒になってて、iPadで手書きしてますけど。

「エッジタイトル」
エッジタイトル - コクヨS&T
ページの両端にタイトルを書き込むスペースがあるというやつ。こういう風な事を前に試したことがありましたけど、面倒でやめちゃいました。いやこれ結構面倒くさいと思いますよ。話題や日付が変わったらページを変えるくらいやった方が参照もしやすいし、書きやすいんじゃないかなぁ。みたいな話はまた別に記事書きますけども。
仕事がこれだけ細かい単位なら、日記帳とかスケジュール帳を使ったほうがいいんじゃないかなぁ。余白も余白で情報だと思うし。

「文庫本ノート」
文庫本ノート - コクヨS&T
文庫本サイズのノート、やっぱ持ち歩くならこのサイズだろうという感じですね。間違いないです。
持ち歩くということはそれなりに痛むのでブックカバーをつけたくなる。文庫本サイズだと市販品がたくさんあるし、お気に入りのものも使えるかもしれない。そういう用途を想定してペンホルダーがついたのも結構ありますしね。このページでも紹介されてます。
あとインデックス用の罫がついてるのもいい。インデックスつけるのよくやるので、これがやりづらいノートはそもそも使わないし。しおりがついてるのはよくわかんないけど、日記帳にするなら便利かな。
丈夫そうだし。
とはいえ今のところノートを使うのは会社でだけで、社内でもずっと自席に座っているので、今の私には無用の長物かなぁ。もっと忙しくなったら使おう。と思った。まあそうならないようにしよう。いやこれが必要な仕事もそれはそれで楽しいかもしれませんけど。

これが必要な仕事が楽しそうといえば、モレスキンのメモポケットってのがありますね。
MOLESKINE モレスキン メモポケット
メモを挟むやつ。楽しそうというか、これが必要な仕事って何なんだろうと。
これ何を入れるんだろう。メモとか名刺とかかな。名刺はわかるけども、メモってなんだ。
おそらくこれはシステム手帳の変形なんじゃないかと思うんですが、どんなもんなんでしょうね。テーマで分けるんだったら別にシステム手帳でいいので、頻繁に人とやりとりするとか。名刺を表紙にしてメモを入れるとして、同時に6団体と仕事ができる。たぶんシステム手帳と組み合わせて、こまめに母艦に情報を移しながら戦う感じになると思うんですが、フリーランスの人とかかな主に使うのは。どういう艦隊を組むのかちょっと興味ある。

「PARACURUNO(パラクルノ)」
PARACURUNO[パラクルノ] - コクヨS&T
側面が斜めにカットされていてめくりやすいらしい。アイデア商品ですね。いかにもコクヨらしい。
これも時系列の、日記に良さそうかも。これもA6文庫本サイズですね。いいんですが、私はあんまし過去のメモを見返したり探したりしないので、形は普通のがいいかなぁ。でも日記だったら使いたいかな。A5があったらちょっと使ってみたい。

こんなの考えてるだけで余裕で1日つぶせたりするのでモンハンは進みません。

Haskellで可変長引数

可変長引数を作りたかったわけではないんだけど、というか何がしたかったんだかよく覚えていないんだけど、こういうのを作った。

class Test a where
    test :: a -> Int 
    test = const 0

instance Test a => Test (b -> a) where
    test f = 1 + test (f undefined)

instance Test Int 

Test型クラス。

> let c = undefined :: Int
> test c
0
> let f = undefined :: Int -> Int
> test f
1
> let g = undefined :: Int -> Int -> Int
> test g
2

test関数に与えた関数の引数の数を数えられます。
ただし、与える関数の戻り値の型がTestのインスタンスになっていないと使えません。

これ、なんとなく面白くはあるんだけど、何の意味があるんだ。使い所もよくわかんなくなっちゃったなぁなんて思っていたんですが、printfがこの仕組で作られていました。
http://hackage.haskell.org/package/base-4.6.0.1/docs/Text-Printf.html
うわあああこれprintfじゃないか!
あとQuickCheckのpropというかTestableクラスも思いっきりこの仕組で実装されていました。
http://hackage.haskell.org/package/hspec-1.7.2.1/docs/Test-Hspec-QuickCheck.html
http://hackage.haskell.org/package/QuickCheck-2.6/docs/Test-QuickCheck-Property.html#t:Testable
誰だ使い所無いとか言ったやつ。

特にQuickCheckなんて最近も使ってるし、なんで疑問に思わなかったのか。これ作った人頭いいな。

まあそれはいいとして、

class Arg a where
    f :: c -> a

みたいな型クラスを定義して、

instance Arg a => Arg (b -> a) where
    f = ...

みたいなインスタンスを作ると可変長引数関数が作れるみたいです。
(b -> a)がArgのインスタンスなら(d -> b -> a)というか(d -> (b -> a))もArgのインスタンスになるので。

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