Iteratee という概念は、Haskell 界に適切な資源管理と合成可能な IO をもたらした。そして、以下の3つのパッケージが乱立することになった。
- iteraee
- enumerator
- iterIO
昨年の ICFP の際、Iteratee の生みの親である Oleg さんに「この状況をどう思っているのか」と聞いてみた。曰く「とてもよい状況です。いくつかの実装が現れ実際に使われることで、本当に必要な機能が分かるでしょう」。
もしかすると、Conduit によって彼の願いがもう実現されたのかもしれない。
Iteratee には何が足らなかったのか?
以下は、enumerator の使用経験基づく考察だが、たぶん Iteratee 全体に言えると思う。
- Iteratee で資源を割り当てられない
- Michael Snoyman さんの不満
- 例外処理が大変
- liftIO と catch を使いたいのに、tryIO と catchError を使わないといけない
- Enumerator という情報源を自由に引き回せない
- 僕を含めた Proxy サーバを作っている人の不満
1) は簡単に分かる。enumFile はあるのに、iterFile がない。
2) は、コードを書いてみれば、すぐに嫌になる。
3) は難しいが説明してみる。読飛ばしてもらってもかまわない。WAI + Warp + http-enumerator で Proxy を作るとする。以下のようにデータがやり取りされる。
---a--> ---c--> Brower Proxy Server <--b--- <--d---
ここで、HTTP の POST を考える。HTTP Request のボディは、a から c へ固定長のバッファを使ってストリーミングされてほしい。HTTP Response のボディも d から b へ固定長のバッファを使ってストリーミングされてほしい。
WAI では、a の HTTP Reuqest ヘッダが引数として受け取ることができるが、a のボディは enumerator として配管の下に埋まっている。このボディを c を作成する http-enumrator に渡す方法が存在しない。逆に d から b へのストリーミングは、何もしなくても実現できる。
つまり、WAI 上に Proxy を作成すると、上りはストア&フォワード、下りはストリーミングになる。これは、大きな HTTP ボディを持つ DOS に対して、あまりにも脆弱だ。
結局、Iteratee は簡単な問題に利用するのはよかったが、複雑な問題を解くには窮屈な感じだった。
Iteratee では、Enumerator は Iteratee というオートマトンを駆動する主役であって、自分自身がどこかに引き渡されることなんて想定されていなかった訳だ。
Conduit
昨年の11月頃に、Michael さんに3) の不満を伝えたところ、最初は理解されなかった。幸運なことに、そのころあるイタリア人が Proxy を作っていて、僕の問題を理解してくれた。そして、二人掛かりでの説明が始まった。結局、Oleg さんとか、3 つのライブリの作者とかを巻き込んだ大議論が起こり、enumerator に不満を感じていた Michael さんが猛烈な勢いで Conduit を実装した。yesodweb のブログを見れば、その勢いが分かるだろう。
Conduit は、Iteratee の push 型を改め、pull 型に先祖帰りした。3人の登場人物の名前は以下の通り。
- 生産者:Source
- 消費者:Sink
- パイプ:Conduit
Sink という名前には違和感があるが、水道管を意味する Conduit からの連想と思えば納得できる。
Conduit では、モナドを IO (と ST) に制限しており、IORef (STRef) を使って、資源の解放を管理する。また、上記3つの問題がすべて解決されている。
1) を解決する例:
import Data.Conduit import qualified Data.Conduit.Binary as CB copyFile :: FilePath -> FilePath -> IO () copyFile src dest = runResourceT $ CB.sourceFile src $$ CB.sinkFile dest
2) は、lifted-base の Control.Exception.Lifted が自由自在に使えることで解決されている。2) と3) を解決する例の一部を示す:
import Control.Exception (SomeException) import Control.Exception.Lifted (catch) import qualified Network.HTTP.Conduit as H import Network.Wai -- WAI の Request を HTTP.Conduit の Request へ変換 toHTTPRequest :: Request -> RevProxyRoute -> Int64 -> H.Request IO toHTTPRequest req route len = H.def { H.host = revProxyDomain route , H.port = revProxyPort route , H.secure = isSecure req , H.checkCerts = H.defaultCheckCerts , H.requestHeaders = addForwardedFor req $ requestHeaders req , H.path = pathByteString path' , H.queryString = rawQueryString req -- WAI から Source なボディが取れる! -- enumerator だとこれができない , H.requestBody = H.RequestBodySource len (toSource . requestBody $ req) , H.method = requestMethod req , H.proxy = Nothing , H.rawBody = False , H.decompress = H.alwaysDecompress } where path = fromByteString $ rawPathInfo req src = revProxySrc route dst = revProxyDst route path' = dst </> (path <\> src) -- catch も自由自在だよ revProxyApp :: ClassicAppSpec -> RevProxyAppSpec -> RevProxyRoute -> Application revProxyApp cspec spec route req = revProxyApp' cspec spec route req `catch` badGateway cspec req revProxyApp' :: ClassicAppSpec -> RevProxyAppSpec -> RevProxyRoute -> Application revProxyApp' cspec spec route req = do let mlen = getLen req len = fromMaybe 0 mlen httpReq = toHTTPRequest req route len -- Request のボディをストリーミングしながら転送 H.Response status hdr downbody <- H.http httpReq mgr let hdr' = fixHeader hdr liftIO $ logger cspec req status (fromIntegral <$> mlen) -- Response のボディは自動的にストリーミングになる return $ ResponseSource status hdr' (toSource downbody) where mgr = revProxyManager spec fixHeader = addVia cspec req . filter p p ("Content-Encoding", _) = False p _ = True badGateway :: ClassicAppSpec -> Request-> SomeException -> ResourceT IO Response badGateway cspec req _ = do liftIO $ logger cspec req st Nothing return $ ResponseBuilder st hdr bdy where hdr = addServer cspec textPlainHeader bdy = byteStringToBuilder "Bad Gateway\r\n" st = statusBadGateway
備考
WAI 1.0 と Yesod 1.0 は、Conduit ベースになります。