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
が良い選択だろう.