Haskellの例外周りの話
RIOのベストプラクティスから.これはYesodの開発の知見から作られている.FP Completeの元記事がある.
IOの場合
結論
safe-exceptionパッケージを使おう!
ここによくまとまってる
transformers と mtl である. transformers は lift という下位のモナドのアクションをでかいモナドのアクションに持ち上げてくれるメソッドが定義された MonadTrans クラスと,各モナド変換子が定義されたパッケージである.しかし, MonadTrans は StateT や ReaderT などを持ち上げることができないため,これらがモナド変換子スタックの最上位であることを要求する. mtl は,モナド変換子スタック(要は上で言っているモナド変換子を組み合わせてできたでかいモナドのこと) m に対する型クラスとして, ask などの各種アクションを MonadReader などの MonadHoge 型クラスのメソッドとして定義しており, transformers の MonadTrans もreexportしている.これにより, MonadTrans の代わりに MonadState などを経由してアクションが呼べるため,先程の MonadTrans の制約に縛られずに自由にモナド変換子を使うことができる. transfomers で定義されているものと mtl で定義されているものがある.これは前述の MonadHoge を定義するのに MultiParamTypeClasses と FunctionalDependencies 拡張が必要なため,GHC(Huges)以外では使えないと言う問題がある.したがって, transformers は標準のHaskellでも使用できる MonadTrans とそれで必要十分なモナド変換子( MaybeT など)が, mtl では MonadHoge とそれのインスタンスとなるようなモナド変換子( ReaderT など)が定義されている. m )にし,型クラス制約としてcomputationを表す(e. g. MonadReader env m )ような設計手法をmtl styleと呼ばれており,現状のベストプラクティスとなっている雰囲気がある.自分でモナドを作る際はそのように計らうと良いだろう. Data.Hoge.Lazy だとか, Data.Hoge.Strict とかがあり,中身の関数や型は一見同じように見えて混乱する.これはいわゆる遅延データ構造か正格なデータ構造かの違いである. undefined が持てたり,フィールドの一部がからでも動かすことができたりと便利である.モナドはメソッドの実装やアクションがlazyかstrictかという意味だったりするので注意しよう. String である.これが [Char] の型シノニムであることはあまりにも有名である.遅延リストという糞効率の悪いものに Char を突っ込んで文字列を表現しているので,長い文字列を扱うと加速度的にパフォーマンスが悪くなっていく. bytestring の ByteString である.これは内部的にはチャンクへのポインタとオフセットと長さのトリプルで,いかにも効率がよさそうである.このByteStringの1byteを Char に相互変換してくれるのが, Data.ByteString.Char8 である.O(1)で結合してくれる fast-builder などもあり,便利であるが,マルチバイト文字列を扱う場合はUTF-8にエンコードされるため,文字列の長さが正確に測れない,GHCの OverloadedStrings 拡張でリテラルを書くとUTF-16エンコードなので正しく読み込まれないなど,マルチバイトまで含めた文字列を扱うには貧弱である. text の Text を使うと良い. ByteString と違い,2byteのバイト列を使った実装になっており,内部的にはUTF-16を第一級に扱っている.複数のエンコーディングにも対応している. Text を使い,効率が要求される場面や単純にバイト列を扱いたい場合のみ ByteString を使うのが良いだろう.Alt Preludeである rio は基本的に文字列は Text として扱い,更に基本の関数の出力は Builder (メモリに書き込みする関数として文字列を持っておき,文字列の合成を関数合成に置き換えることでO(1)で結合するというテクニック)にするという徹底ぶりで高効率な文字列操作を実現している.使ってみるのもよいだろう. String に関しては,エラーメッセージなどごく限られた使用に留めるべきである(適当に Show のインスタンスを書くと効率が死ぬほど悪くなる問題). GHC.Exts に定義されている Array# というUnboxed値である(つまり生のメモリ表現.後述する). Array は上界と下界と長さの情報が付加された Array# で, Vector はオフセットと長さの情報が付加された Array# である. -O2 でコンパイルしていれば正格なフィールドはUnboxed値が入るように最適化され効率がよくなる.そういう意味でも前述した,特に理由がなければ正格なデータ構造を使う・定義するというのは大事である. Array や Vector には,このようなUnboxed値に特化した配列が用意されている.特にUnboxed Vectorは中身がバイト列である上,関数の一部が裏で PrimMonad を介してアセンブラレベルのGHCの組み込み関数に置き換えられているため,非常に高速に動作する.配列のような大きなデータは省メモリのためにも,できるだけUnboxedな Vector の使用を試みるべきだろう.自分で定義したデータ型を Unbox のインスタンスにしたい場合, vector-th-unbox にある derivingUnbox マクロが便利である. ST モナドや IO モナド越しに使えるMutableな配列も用意している. Vector を使うとよいだろう.また,ソートをしたいときは,Mutable Vectorを使う必要がある( vector-algorithms など). safe-exception は,例外を投げるときに非同期例外かどうかを型情報に加えてから投げるため,これらによる分岐が可能であり,関数としても通常の catch は非同期例外を補足しないようにしている.非同期処理を行うのであれば, safe-exception が良い選択だろう.