Haskellでいい感じのHTTPパーサライブラリはない?
「いい感じの」とはずいぶん主観的だが、だいたい以下のようなHTTPパーサが欲しくなってちょっと調べていた。
- 基本的に(Lazy?)ByteStringに対して作用する。
- 非IO。
- HTTPリクエストとレスポンスをパースできる。
- HTTPボディも引っこ抜いてくれる。
- HTTPボディは一定メモリでストリーミングできる (具体的な実現形態はうまくイメージできないが・・)
- Transfer-Encoding: chunkedなHTTPボディをデコードしてストリーミングできる。
- 一つのByteStringに複数のHTTPリクエスト or レスポンスが含まれている場合もパースできる。
- できればContent-Encoding: gzipなどの圧縮コンテンツを解凍するオプションがある。
- さしあたりHTTP/2は非対応でいい。
イメージとしてはこちらで質問されているようなものが近い。
が、意外とこういうものがないかも? 少し調べてみた。
salvia-protocolパッケージの提供するパーサ。Stringがターゲットなのでボツ。
http-clientの内部モジュール。
'parseStatusHeaders'や'getResponse'関数があるが、いずれもConnectionをターゲットとしたIO関数。
attoparsecのexamplesに同梱されているパーサ。
見たところContent-Lengthヘッダの解釈などもやっていないようなのでボツ。
warpの内部モジュール。
'recvRequest'関数があるが、やはりConnectionをターゲットとしたIO関数。そこから呼ばれている'parseRequest'関数などもIO。
ということでHTTPクライアント、サーバの内部実装を簡単に調べてみたが、意外とIOに密結合した作りになっているようだ。ボディストリーミングをやる都合上か、性能を高める都合上そうなっているのかもしれない。
別にIOでも構わないといえば構わないが、本質的にIOじゃなくてよいものをIOにするのはなんとなくモヤる。
他に何かないだろうか。
lifted applicative style
モナドを使った演算を書いている時は、applicative styleを使うと記述量を少なくできて便利だ。
>>> (++) <$> getLine <*> getLine hoge foobar "hogefoobar"
使う関数が最終的にモナドを返す場合、最後にjoinしてやればいい。
>>> let catPut a b = putStrLn ("== " ++ a ++ b) >>> join $ catPut <$> getLine <*> getLine hoge foobar == hogefoobar
さて、上記と同様のことをmonad transformerで拡張した文脈中ではどうするか、ちょっと悩んでしまった。
正解は(lift =<<)を使えばいい。
>>> let action = lift =<< catPut <$> ask <*> lift getLine >>> runReaderT action "hoge" foobar == hogefoobar
(lift =<<)の型はこうだ。
>>> :t (lift =<<) (lift =<<) :: (Monad (t m), Monad m, MonadTrans t) => t m (m b) -> t m b
見ての通り、join :: Monad m => m (m b) -> m b によく似ている。
ちなみにcatPutへの引数がモナドではない素のデータだとこうなる。
>>> let action' = lift $ catPut "HOGE" "FOOBAR" :: ReaderT String IO () >>> runReaderT action' "hoge" == HOGEFOOBAR
素のデータの場合の記法から、($)を(=<<)に、関数適用を(<$>)と(<*>)に書き換えればapplicative styleになる。
()は(.)の代わりに使える
例えばこういう関数群があったとして、
toStr :: Float -> String toStr = show length' :: String -> Int length' = length isBig :: Int -> Bool isBig = (> 5)
Haskellでは($)演算子を使ってデータに対して次々に関数を適用できる。
isBig $ length' $ toStr $ 32.1
($)演算子は右結合なので、以下のように結合している。
isBig $ (length' $ (toStr $ 32.1))
逆に、左結合を強制すると型が合わずにエラーになる。
((isBig $ length') $ toStr) $ 32.1 -- エラー
さて、関数に流し込みたいデータがFunctorの中に入っている場合、Control.Applicativeモジュールの(<$>)演算子が便利である。
isBig <$> length' <$> toStr <$> Just 32.1
しかし(<$>)演算子は左結合である。つまり以下のように結合していることになる。
((isBig <$> length') <$> toStr) <$> Just 32.1
しかも(<$>)演算子の場合、右結合を強制してもちゃんと動作する。
isBig <$> (length' <$> (toStr <$> Just 32.1))
なぜ($)と違って左結合で動作するのだろうか。
ここで、(isBig <$> length')の型を調べてみる。
*Main> :i isBig <$> length' isBig :: Int -> Bool -- Defined at ops.hs:11:1 (<$>) :: Functor f => (a -> b) -> f a -> f b -- Defined in `Data.Functor' infixl 4 <$> length' :: String -> Int -- Defined at ops.hs:8:1 *Main> :t isBig <$> length' isBig <$> length' :: String -> Bool
(<$>)の第2引数はlength'という関数である。そして関数はFunctorである。つまりこの場合、(<$>)は
(<$>) :: (Int -> Bool) -> (->) String Int -> (->) String Bool
このような型に束縛されていて、結果的に(String -> Bool)という型を返したのだ。
ちなみに上記の型を一般化すると以下のようになる。
(<$>) :: (b -> c) -> (->) a b -> (->) a c
これは関数合成演算子(.)と同じである。
*Main> :i (.) (.) :: (b -> c) -> (a -> b) -> a -> c -- Defined in `GHC.Base' infixr 9 .
必須引数とオプショナル引数が混在した名前付き引数渡し
Haskellで名前付き引数(named arguments/parameters)のようなことをしようと思ったらレコード構文を活用するのが一般的である。
data Args = Args { arg1 :: Int, arg2 :: Int, arg3 :: String } func :: Args -> Int func = undefined main = print $ func $ Args { arg1 = 100, arg2 = 50, arg3 = "foobar" }
この際、デフォルトのArgsレコードを用意してHaskellのレコード更新構文を使うことで、引数をoptionalにすることができる。
defArgs :: Args defArgs = Args { arg1 = 0, arg2 = 0, arg3 = "" } main = print $ func $ defArgs { arg2 = 99 }
更新されていないフィールドにはデフォルトの値が用いられる。
data-defaultパッケージへ依存しても構わないのであれば、Default型クラスを使うとより利便性が向上するだろう。
import Data.Default (Default(def)) instance Default Args where def = defArgs main = print $ func $ def { arg2 = 99 }
全ての引数がoptionalであればこれで構わない。しかし、一部の引数を必須(mandatory)としたい場合、どうすればよいだろうか。つまり、
data Args = Args { m1 :: Int, m2 :: Int, o1 :: String }
このような引数構成で、m1とm2は必須、o1はオプショナルとしたい場合である。
まず、先述のdefArgsに引数を取らせる方法が考えられる
data Args = Args { m1 :: Int, m2 :: Int, o1 :: String } defArgs :: Int -> Int -> Args defArgs m1_val m2_val = Args { m1 = m1_val, m2 = m2_val, o1 = "" } func :: Args -> Int func = undefined main = print $ func $ defArgs 4 5
しかしこれでは必須引数が結局positional argumentになってしまい、必須引数が増えた場合に可読性が低下する。
必須引数をundefinedとする手もある。
import Data.Default (Default(def)) data Args = Args { m1 :: Int, m2 :: Int, o1 :: String } instance Default Args where def = Args { m1 = undefined, m2 = undefined, o1 = "" } func :: Args -> Int func = undefined main = print $ func $ def { m1 = 10, m2 = 20 }
こうしておけば、必須引数を設定し忘れた場合、その値を参照しようとした段階で実行時例外が発生する。
しかし、この方法では必須引数の設定し忘れをコンパイル時に検出できない。Haskellを書いている以上、プログラムの正しさはなるべくコンパイル時に検証したいものである。
そうなると、やはり必須引数とoptional引数を分けたほうがいいのだろうか。
import Data.Default (Default(def)) data Args = Args { m1 :: Int, m2 :: Int } data Opts = Opts { o1 :: String } instance Default Opts where def = Opts { o1 = "" } func :: Args -> Opts -> Int func = undefined main = print $ func Args { m1 = 10, m2 = 20 } def
上の例では、必須引数をArgs型に、optional引数をOpts型にまとめ、Opts型のみをDefaultのインスタンスにしている。また、func関数はこの2つを引数としてとる。Args型データをレコードコンストラクタで作成する際にフィールドの数が足りない場合、ghcは警告を発する。
cabalを使ってHaskellのプログラムを作り始める時のメモ
公開しようがしまいが、Haskellでプログラムを作る時はcabalを全面的に使うといろいろメンテしやすい。しかしその使い方(特に.cabalファイルの書き方)は自明ではないということで、いろいろ調べたのでメモ。
.cabalファイルには、とりあえずlibraryセクションは書いておく
executableセクションがあろうがなかろうが、自分で書いたモジュールのほぼ全てをlibraryセクションに突っ込んでおくのがよいようだ。executableセクションやtest-suiteセクションで自分の書いたモジュールを参照するためには、各セクションのbuild-dependsに自分自身のパッケージ名を書いておく。
なお、executableパッケージを作る場合、libraryセクションがなくてもtest-suiteのhs-source-dirsにexecutable用のコードを入れたディレクトリを指定すればテストを書くことができる。しかしこれには以下の欠点がある。
- executable用のコードはexecutableのビルド時とtest-suiteのビルド時の2回、コンパイルされてしまう。
- test-suiteのbuild-dependsにはテストコードと参照するexecutable用のコードの両方の依存パッケージを書く必要がある。
両方共DRY原則に激しく反するため、精神衛生上よろしくない。libraryセクションを使うべきである。
参考:
- haskell - How to reduce duplication in the build-depends fields of a .cabal file? - Stack Overflow http://stackoverflow.com/questions/10163604/how-to-reduce-duplication-in-the-build-depends-fields-of-a-cabal-file
- Haskellのexecutableなプログラムでテスト自動化 - Qiita http://qiita.com/arowM/items/1a56cc208e418829a097
テストで使うモジュールは全てexposeする
上記のようにlibraryセクションを設けてtest-suiteでそれを参照する場合、テストコードから参照できるのはlibraryセクションのexposed-moduleに書いたモジュールのみである。しかし(あまりよいことではないかもしれないが)、ユーザーに見せないプライベートな関数をテストしたり、テスト中で内部ユーティリティを活用したりしたいことはしばしばある。
そのような、「テストでは使うがユーザーには見せない」モジュールをうまいこと.cabalで指定する方法は今のところないらしい。そういったものも全てexposeする必要がある。exposeしてはいるがユーザー向けではないモジュールには名前に"Internal"を含めたりするのが慣習となっている。
参考:
- haskell - cabal test for not exposed modules - Stack Overflow http://stackoverflow.com/questions/25618244/cabal-test-for-not-exposed-modules
doctestとhspecでテストを記述する
参考:
cabal initやhiでひな形を生成する
cabal initコマンドを使うと、パッケージのひな形を作成することができる。しかしcabal initは今のところtest-suiteセクションやテストのエントリポイントを自動生成しないため、毎回手で{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
と書くことになって精神衛生上よろしくない。
そこでひな形の生成にはhiというコマンドを使うとよいようだ。このコマンドはデフォルトでHspecのエントリポイントを生成する。また、ひな形の実体はgitレポジトリであるため、カスタマイズしたい場合はgithub上でforkしていじればOK。
参考:
- Haskellプロジェクトのひな形生成ツールを作った http://fujimuradaisuke.com/haskell_advent_calendar_2013.html
- fujimura/hi https://github.com/fujimura/hi
forkIOがmask状態を引き継ぐ意味が分からない
(注: この記事はghc 7.6.3, base-4.6.0.1で検証している)
HaskellのIOモナドには(フツーの命令型言語で言うところの)例外の仕組みが備わっているが、この例外には2種類がある。
- 同期例外(synchronous exception): スレッド自身が行う処理によって発生する例外
- 非同期例外(asynchronous exception): スレッドの外部から(
throwTo
関数などによって)送りつけられる例外
非同期例外はいつ発生するのか本当に予測不可能な例外であり、どちらかというとPOSIXのシグナルにイメージとしては近いと思う。
不用意なタイミングで非同期例外が発生するとプログラムの状態がおかしなことになってしまうため、Haskellには非同期例外の発生を一時的に抑制する仕組みがある。
mask_ :: IO a -> IO a
mask_
関数は引数としてIOアクションをとり、非同期例外の発生を抑制した(マスクされた)アクションに変換して返す。
マスクされたアクションの実行中は、そのスレッドで非同期例外が発生することはない。その間、throwTo
関数で非同期例外を送信したスレッドは、送信先スレッドのマスクが解除されて非同期例外がちゃんと発生するまでブロックする(これはghcに固有の動作かもしれない)。そのため、マスクされるアクションの範囲は必要最小限に抑えることが重要となる。
さて、Haskellではスレッドを作る時は一般的にforkIO
関数を使う。
forkIO :: IO () -> IO ThreadId
forkIO
は引数としてIOアクションをとり、それを別スレッドで実行し、呼び出し元にはスレッドIDを返す。
ここで重要なのは、forkIOで発生させたスレッドは親スレッドのマスク状態を引き継ぐという点である(これはghcに固有の動作かもしれない)。
そのため、親スレッドが非常に限定的な範囲でマスクしていたとしても、たまたまマスクした状態で子スレッドを作った場合、子スレッドは全体がマスクされることになる。
-: マスク解除状態の処理 *: マスク状態の処理 親スレッド: -----------***------------------- ... | [forkIO] | 子スレッド: ********************* ...
さらに悪いことに、Haskellの例外処理関数の中には与えられたアクションをマスクして実行する類のものがある。
例えば、bracket
関数の第1引数(リソース獲得アクション)と第2引数(リソース解放アクション)はマスクされる。これは以下のコードで検証できる。
import Control.Exception (bracket, getMaskingState) showMask label = do state <- getMaskingState putStrLn (label ++ ": " ++ show state) main = bracket (showMask "before") (const $ showMask "after") (const $ showMask "body") -- before: MaskedInterruptible -- body: Unmasked -- after: MaskedInterruptible
したがって、例えばリソース獲得アクションとしてforkIO
を書いてしまうと子スレッドは全体がマスクされる。
この事実はドキュメントには書かれていない。そもそもマスク状態は型に表れないので、ユーザは関数へ渡したアクションがマスクされるのかされないのか確実に見極めることはできない。
このように、マスク状態の引き継ぎは子スレッド全体が意図せずマスクされてしまう事態を引き起こすが、そもそもなぜforkIO
の仕様がマスク状態を引き継ぐようになっているかが理解できない。非同期例外はスレッドごとに独立なのだからわざわざ引き継ぐ必要はないのではないか。
親スレッドのマスク状態に関わらず子スレッドのマスクを解除するには、とりあえず以下のようにすればよい・・・と思う。
{-# LANGUAGE CPP #-} import qualified Control.Concurrent as CC #if MIN_VERSION_base(4,3,0) #else import qualified Control.Exception as CE #endif forkIOUnmasked :: IO () -> IO CC.ThreadId #if MIN_VERSION_base(4,4,0) forkIOUnmasked action = CC.forkIOWithUnmask (\unmask -> unmask action) #elif MIN_VERSION_base(4,3,0) forkIOUnmasked = CC.forkIOUnmasked #else forkIOUnmasked action = if CE.blocked then CE.unblock $ CC.forkIO action else CC.forkIO action #endif
マスク周りのAPI(以前は"ブロック(block)"と呼ばれていた)はいろいろと変更になっているため、古いbaseパッケージもサポートしようと思ったらバージョンごとに実装を変える必要がある。
正直、何か見落としている点があるかもしれないので上記のコードもそれほど自信はない。。