読者です 読者をやめる 読者になる 読者になる

Haskellライブラリ所感2016

(これは Haskell Advent Calendar 2016 の7日目の記事です)

今年使ったり調べたりした Haskell ライブラリを広く紹介していく企画です。 あくまで今年使ったものなので新しいものばかりではないです。 また記事の性質上、紹介するものが偏っていてもご容赦ください。

Hackage にはすごい数のライブラリが登録されていて、 頼もしいことですが目が回りそうにもなってしまいます。 この記事が Haskell のライブラリを調べる上での指針になったら幸いです。

なおこの企画と方向性が似ているものとして State of the Haskell ecosystem ( 2016年2月版 ) があります(英語)。 これは Haskell を取り巻く環境を知る上で非常によいドキュメントです。 ただ各ライブラリについては名前を挙げるだけにとどめられています。 この記事ではもう少しライブラリの内容に踏み込んだ説明をしていきます。

目次

文字列

bytestring, blaze-builder, bytestring-conversion, base64-bytestring
text, text-format, text-icu
utf8-string
shakespeare

Haskell の標準の String 型は Char リストのエイリアスなので、 文字列として使うには効率が十分でないことが多いです。 そのため効率的な文字列型として text や bytestring があります。 かといってリストのメリットもなくしたくないので text と bytestring は Lazy 版を提供しています。 まとめると

  • String = [Char]: 文字のリスト(UCS-4 (UTF-32?))
  • Data.Text: 文字列 (UTF-16)
  • Data.Text.Lazy: 文字列のリスト (UTF-16)
  • Data.ByteString: 文字列 (バイト列)
  • Data.ByteString.Lazy: 文字列のリスト (バイト列)

上記はあくまで内部的な話で、それぞれのインターフェースはほぼ同じように設計されていてどれも文字列として扱えます。 また OverloadedStrings 拡張によってどれも文字列リテラルから生成することができます。

上記に加えて、text と bytestring は Builder というのも提供しています。 イミュータブル文字列は連結を繰り返すと効率が悪いので専用の型があるわけです。 JavaStringBuilder のようなものだと考えておけばいいと思います。

blaze-builder は bytestring 用のビルダーでしたが bytestring 自体がビルダーを提供するようになったために現在は積極的に開発されていないようです。 互換性のために残されています。

bytestring-conversion は自作の型に独自にシリアライズ・デシリアライズを実装するのに便利です。 普通は JSON とか既存のものを使えばいいですがたまに独自のシリアライズが必要になることもあります。

base64-bytestring は bytestring に Base64 エンコーディングを提供します。

text-format は文字列のフォーマット出力(%d ではなく {} を使う方)を提供します。

text-icuICU ライブラリへのバインディングです。 自分は正規表現の機能のために使っていました。 Haskell はパーサコンビネータが優秀なので正規表現が必要になることはあまりないのですが、 今年はアプリのインターフェースとして正規表現をユーザに提供するという機会がありました。

utf8-string は String や bytestring に UTF-8 のインターフェースを提供します。 ただし text が UTF-8 との相互変換を提供しているのであまり使う機会はない気もします (自分は使っていません)。

shakespeare はテンプレートエンジンです。 自分は Haskell でヒアドキュメントもどきをやりたいためだけに使っていました。

ロガー

monad-logger
fast-logger
monad-log
katip

Haskell のロガーとして最も広く使われているのは monad-logger だと思います。 独立して動作させる以外にもお手元のモナドにログ機能を導入することもできます。

fast-logger は monad-logger や他のいくつかのロガーのバックエンドとして利用されているロガーです。 これを単体で使うことはほぼないと思われますが、 monad-logger で少し凝ったことをやろうとすると fast-logger の API も使うことになったりします。

monad-log は monad-logger に不足している機能 (JSON とかタイムスタンプとか) を追加し、さらに他のモナドとより組み合わせやすくしたもののようです。 これもバックエンドに fast-logger を使っています。 今年登場した新しいロガーですが数ヶ月前から更新が止まったままです……。

katip は珍しく fast-logger を使っていないロガーです。 速度というよりはリッチなログ出力を重視しているようで、 自分が知る限り今最も多機能なロガーです。 また他の言語のロガーとも使用感が近いです。 もちろん JSON とかタイムスタンプに対応しています。

このあたりの話は 別の記事 にも少し書きました。

例外

safe-exceptions
exceptions, enclosed-exceptions

結論から言うと safe-exceptions を使っておけばいいです。

Haskell の標準の例外機構は非同期例外まわりが大変なのと IO に制限されるのが面倒というのがあって、 それをなんとかするために exceptions とか enclosed-exceptions とかが開発されました。

現在は safe-exceptions が決定版と言えるでしょう (この話題については別の記事を書きました)。

なお safe-exceptions のチームは並行・並列処理に async (後述) パッケージを使うことを推奨しています。 そうすることで safe-exceptions のメリットを最大限享受できます。

ちなみに Haskell の例外は非検査例外ですが、 mtl (後述) の Control.Monad.Except を使えば検査例外の模倣ができます。 ただし Haskell の例外機構とは独立したものなので注意が必要です。

データ

aeson, yaml
ini
lens
xml-conduit, xml-lens
time, thyme
containers, unordered-containers

aeson は HaskellデファクトスタンダードJSON ライブラリです。 JSON を扱う他のライブラリは大体これに依存しています。 データ型を書くと自動的に JSONリアライザ・デシリアライザを用意してくれたりします。 すごい。

yamlYAML ライブラリですが、独立したものというより aeson の YAML 向けインターフェースといった感じです。 aeson と同じ API で操作できます。

ini は INI ファイルの読み書きをサポートするライブラリです。

lens は Haskell の「入り組んだデータ構造の奥底の値を読んだり書いたりするのが大変」 という問題に対する救世主です。 しかし代償として多くの演算子を導入します。 まあ基本的な一部の演算子をインポートするだけでも十分メリットがあるというのが私の実感です。

xml-conduit は XML の読み書きをサポートするライブラリです。 名前に conduit とありますが conduit ライブラリ(後述)のことは特に意識しなくても使えます。 xml-lens は xml-conduit に lens のインターフェースを導入します。 XML はまさに lens が得意とする複雑なデータ構造(であることが多い)ので適任です。

time は日付時刻ライブラリのデファクトスタンダードです。 thyme は time をより高速で扱いやすくし、さらに lens インターフェースを追加したものらしいですがまだ触っていません。 なんで日付時刻に lens? と思うかもしれませんが、 time の ZonedDate 型とかは込み入ったデータ構造になっていて時刻を0秒にするとかが結構大変なんですよね。 そういう意味では lens インターフェースをつけたいという動機は確かにあります。

containers と unordered-containers は Map とか Set とかそういうデータ構造を提供するライブラリです。

データベース

relational-record
HDBC, HDBC-mysql
persistent
persistent-relational-record

relational-record を使うと Haskell の言語内 DSLSQL クエリを組み立てることができます。 SQL クエリに型をつけられるというのがまずすごいし、 do 構文内の DSL なので let とか <- でクエリ構築中に変数を作れるのが結構便利です。 さらにコンパイル時に DB にアクセスしてテーブル定義から Haskell のデータ型を作ってくれる機能もあります。 ただちょっと自分の場合は DSL に慣れるまで時間がかかりました

国内の某 ISP の一部サービスはこれで動いているらしい。 実装はファントムタイプのお化けみたいな感じなのでつまりお化けのお化け。

relational-record は実際に DB と通信する部分では HDBC を使うことが想定されています。 HDBC-mysql は長らく開発が止まっている状態だったのですが最近新しいメンテナが入ったみたいなのでよかった。

公平のために言っておくと、 RDB 関係では persistent が今一番有名なライブラリだと思います。 これは Yesod の陣営が作っているので一定の信頼が置けます。 自分はこちらは使ったことがないです。

あと最近 persistent-relational-record という persistent と relational-record を連携させるライブラリがリリースされました。

並行・並列

async, lifted-async
monad-par
stm

async は Haskell の軽量スレッドを安全で扱いやすくしてくれます。 スレッドをタイムアウトさせるとか複数スレッドで一番先に結果を返したやつを採用して他を kill するとかも簡単にできる。 lifted-async は async が IO ベースだというのが使いづらい場合に便利。

monad-par は並列計算文脈 Par モナドを導入します(こう書くと必殺技っぽい)。 たぶん async より決定的な計算をするのに向いています。

stm はソフトウェアトランザクショナルメモリのライブラリです。 軽量スレッド間での通信を安全にしたいときに便利。

なおこのあたりの話は Haskellによる並列・並行プログラミング というすばらしい本(日本語訳)にまとまっています。

通信

wreq
req
amazonka, aws

高レベルの HTTP クライアントとしては wreq が有名です(たぶん wget のオマージュでしょう)。 lens ベースの API を備えていて GET とか POST とかを簡単に投げることができます。 しかし残念ながら wreq は現在開発が止まっているようです。

そこで出てきたのが req です。 現在は HTTP クライアントとしてはこれを採用するのが良さそうです。

req が wreq と比べて足りないところは、 wreq が備えていた AWS の HTTP API リクエスト署名の機能がないことくらいです。 これについては req の作者も考えているようですが今のところありません。

まあ AWS については amazonka シリーズや aws などの専用のライブラリがあるのでそれほど困らないかもしれません。

ストリーム・リソース

conduit
resourcet

Haskell のリストは最初から遅延ストリーム的にも使えるので、 なんでストリームライブラリが必要なんだと思われるかもしれません。 しかしながら、ストリームデータの生成元リソースを解放する必要があるとか、 ストリーム処理中に例外を吐いて失敗する可能性がある場合などは、 リストでは制御が難しくなってきます。

そのようなときには conduit のようなストリーム処理ライブラリが有用です。

resourcet はリソースを解放することを保証するモナドを提供し、 conduit とも連携できます。

テスト

hspec
HUnit
QuickCheck
smallcheck
tasty
HTF
doctest
silently

Haskellユニットテストの関係について何か誤解している向きもいらっしゃるようですが実際のところ Haskellユニットテスト環境は充実しています。

いわゆる普通のユニットテストを提供するのは hspec と HUnit です。 hspec は Rubyrspec インスパイアドなテストライブラリです。 同様に HUnit は JavaJUnit インスパイアドなライブラリです。 (advanced features として JUnit 風の関数ではなく演算子でテストを書くこともできますがかえって分かりにくくなる気がします)。

Haskell で特に発展しているのが性質テスト (property-based testing) というテスト手法です。 入力値を規則に従って自動生成する手法で、 恣意的な入力値を想定してのテストが書けないため関数の性質に着目したテストを書くことになります。 代表的な QuickCheck は入力値をランダムに生成します。 smallcheck は小さいデータ構造については全数テストを生成できるという特徴があります。

上記のようないろいろな種類のテストをひとまとめにして実行できるインターフェースを提供するのが tasty や HTF です。 実は hspec もこの仲間で、 rspec 風のテスト API を提供するだけでなく HUnit や QuickCheck へのインターフェースを持っていたりします。

また Haskell にはユニットテストだけでなく Python 風の doctest もあります。 ドキュメントコメントにあるテストを実行して API ドキュメントが嘘を言っていないか確かめることができます。

テストの種類に依らず使えるユーティリティとして silently があります。 silently は標準出力への出力などをキャプチャすることを可能にします。 標準出力のテストを行いたいときなどに便利です。

モナド

mtl, transformers
monad-skeleton

mtl と transformers はいくつかの汎用的なモナド変換子を提供するライブラリです。 mtl の方がより新しいです。 ( @masahiro_sakai さんから「mtlの方がより古い」とのご指摘を頂きました。 たしかに Haskell Wiki にそのように書いてありました。 まず初期にmtl(mtl V1)があり、その後に新機能のtype familiesを用いたmtl-tfが開発されたものの mtlと重複した部分が多く互換性もなかった。 そこで拡張なしの素のHaskell98で動くtransformersが開発され、 その上にmonads-fdとmonads-tfを別々に構築することで互換性の問題は解決された。 しかしその頃にはすでにmtlが広く使われていたため、transformersとmonads-fdからmtlを再構築(mtl V2)することになった、 という経緯のようです。ご指摘ありがとうござました。 ) よほど古い Haskell 資産があるのでなければ mtl を採用してしまってよいと思います。

monad-skeleton はモナドの自作をサポートするライブラリです。 自分でモナドを作るときというのはたいてい DSL が欲しいときなのですが、 これを使うといとも簡単に DSL が作れてしまいます。 自分はこれでログファイル解析クエリ用の DSL とかを作ってました。 作者による使い方の説明(日本語) もあります。

Webフレームワーク

yesod
servant

yesod はフルスタックの Web フレームワークです。 かなり巨大で取っ付きづらかったのですが stack で雛形アプリケーションを生成できたり、 stackage によって依存問題が起きなくなったので始めやすくなりました。 (stack も stackage も yesod の人たちが作っているわけで、すごいことです)。

あ、年末のコミックマーケットというイベントで Yesod の本に寄稿することになりました。 うまくいけば本が出ますのでよろしくお願いします。

servant は比較的新しい Web フレームワークです。 型レベルでルーティングを記述できるというのが大きな特徴です。 自分が触っていたときは認証とか DB とかのサポートがあまりなかったのですが、 最近になってそのあたりも充実してきたみたいです。

パーサ

parsec, megaparsec
attoparsec

パーサコンビネータライブラリで有名なのは parsec とattoparsec で、 基本的な選択基準はエラーメッセージの分かりやすい方をとるか速度重視の方をとるかです。 parsec の方が比較的エラーメッセージが分かりやすいと言われています。 一方の attoparsec は速度重視です。

parsec はちょっと作りが古いしそれほど長いものをパースしないので私は attoparsec を使っているのですが、 parsec の後継的な megaparsec というライブラリが最近出てきました。

ユーティリティ

file-embed
filepath, path
haddock
resource-pool
auto-update
zlib

file-embed はコンパイル時にファイルを読み込んで bytestring として展開します。 Haskell バイナリに小さい画像などを埋め込みたい場合などに使えるでしょう。

filepath と path は PC 上のパスを扱うためのライブラリです。 path の方がより高レイヤーです。

haddockHaskell コードのドキュメントコメントから API ドキュメントを生成するツールです。 stack にサポートされていて stack haddock コマンドでドキュメントを生成できます。

resource-pool は DB へのコネクションプールのようなものを作ることを可能にするライブラリです。

auto-update は現在時刻のような情報を何度も取得する場合などに便利なライブラリです。 たとえば現在時刻を秒間に何度も取得する可能性があるが時刻の精度は1秒単位でいい場合、 時刻の値をキャッシュしておいて実際の取得は1秒に1回で十分です。 auto-update は値を更新する別スレッドを立てて定期的に実行し、 値を要求されたときにはキャッシュを返す、 というような機能を提供します。

zlib は zlib へのバインディングです。 gzip 圧縮などを行いたい場合に使えます。 純粋関数のような API ですが失敗すると普通に例外を返すので注意が必要です。

所感

今年はいろいろなライブラリを触っていたような気もしますね。 やはり stackage によって依存関係に神経を使わずにライブラリを試せるようになったのが大きい。

一方で、見返すとあまり新しいものには触っていないですね (safe-exceptions と servant くらい?)。 これは見方を変えると Haskell の開発環境が安定してきたということなのかもしれません。

それでは 2017 年もよい Haskell ライフを!

HaskellのロガーKatipを試す

モチベ

Haskell の実行時ログ出力を行うライブラリは monad-logger が一番有名っぽい。 これは Yesod 陣営が開発しているから安心感があるし、バックエンドが fast-logger なので速度も信頼できる。 ただ (自分の調べ方が悪いのかもしれないが) ちょっと自分の用途には機能が足りなかった。 具体的には以下の機能:

  • ログにタイムスタンプを付記したい。
  • ロガーに名前をつけたい。
  • ファイルサイズか日付でログローテーションしたい。

Katip という別のロガーライブラリは機能が豊富のようなので今回はそれを試してみる。

(この記事のHaskell環境: lts-6.23)

Katipのおそらく最小の構成

とりあえず動かしてみる。

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}

module Main where

import System.IO (stdout)
import Control.Monad.Trans (lift)

import Katip

main :: IO ()
main = do
  le <- initLogEnv "MyApp" "prod"                      -- (1)
  hs <- mkHandleScribe ColorIfTerminal stdout InfoS V0 -- (2)
  let le' = registerScribe "stdout scribe" hs le       -- (3)
  runKatipContextT le' () "main" $ do                  -- (4)
    $logTM InfoS "ログ出力"                            -- (5)
    $logTM DebugS "これは出力されない"                 -- (6)
    lift $ putStrLn "ふつうの出力"
    return ()
  return ()
  
-- > [2016-10-30 08:00:31][MyApp.main][Info][kaz-mba-lb8o][23644][ThreadId 7][main:Main app/Main.hs:17:5] ログ出力
-- > ふつうの出力
  1. 新しい LogEnv を作る。ルートロガー名と実行環境名を指定している。
  2. 新しい Scribe を作る。 Scribe というのはログの出力先とログレベルなどの設定を組にしたもの。Katip はログレベルとして severity level (InfoとかErrorとか) だけでなくverbosity level (V0とかV1とか) も指定できる。
  3. LogEnvScribe を登録する。
  4. 作成した LogEnv でロガーのモナドを走らせる。
  5. Info レベルでログを出力する。 $ がついているのは TemplateHaskell でソースファイルの行番号などをログに出力するため。
  6. Debug レベルでログを出力する。しかし LogEnv が Info レベルの Scribe しか持っていないので出力されない。

ロガーのモナドの下でログ出力を行うという点以外はかなり普通のロガーという感じだ。 ファイルに出力したいときは stdout の代わりにファイルハンドルを渡せばいいし、 出力先を増やしたいときは Scribe を追加すればいい。

ログローテーション

Katip は組込みのログローテーションの仕組みは持っていないようだが、 Katip の開発元の Soostone 社は Katip と別に rotating-log というライブラリを公開している。 rotating-log の説明文によると、どうやらファイルサイズベースのローテーションを提供しているらしい。

それならログローテーションはこれでやればいいのかな……と思いきや、

This is so that it can be used to log non-textual streams such as binary serialized or compressed content.

とのことなのでテキストログのローテーションで使うものではないらしい。

一応 rotating-log を Scribe にしてテキストログのローテーションを試してみたが (以下のコード) 参考にしない方がいいかもしれない。

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}

module Main where

import System.IO ( hSetBuffering, hIsTerminalDevice, stdout
                 , BufferMode(..)
                 , Handle
                 )
import Control.Monad (when)
import Control.Monad.Trans (lift)
import Data.Monoid ((<>))
import qualified Data.Text.Encoding as T
import qualified Data.Text.Lazy as LT
import qualified Data.Text.Lazy.IO as LT
import qualified Data.Text.Lazy.Builder as LT
import qualified Data.Text.Lazy.Encoding as LT

import Katip
import Katip.Scribes.Handle (formatItem)
import System.RotatingLog ( RotatingLog
                          , mkRotatingLog, archiveFile
                          , rotatedWrite, rotatedClose
                          )

mkRotatingLogScribe :: RotatingLog -> Severity -> Verbosity -> IO Scribe
mkRotatingLogScribe rl sev verb =
  return $ Scribe $ \i -> do
    when (permitItem sev i) $
      rotatedWrite rl $ T.encodeUtf8 $ LT.toStrict $ LT.toLazyText $ (formatItem False verb i <> "\n")

main :: IO ()
main = do
  le <- initLogEnv "MyApp" "prod"
  rl <- mkRotatingLog "log/myapp" 256 LineBuffering (archiveFile "log/archive/")
  s <- mkRotatingLogScribe rl InfoS V0
  let le' = registerScribe "rotating log scribe" s le
  runKatipContextT le' () "main" $ do
    sequence_ $ replicate 10 $ $logTM InfoS "ログ出力"

-- > $ tree log
-- > log
-- > ├── archive
-- > │   ├── myapp_2016_10_30_17_07_51.852205.log
-- > │   ├── myapp_2016_10_30_17_07_51.852508.log
-- > │   ├── myapp_2016_10_30_17_07_51.85274.log
-- > │   └── myapp_2016_10_30_17_07_51.852956.log
-- > └── myapp.log

ちなみに fast-logger はサイズベースのローテーションの仕組みを持っている。 その一方で monad-logger は fast-logger を使って実装されているにも関わらずローテーションのインターフェースを持っていない。 推測だが、monad-logger を開発している Yesod 陣営が Keter を使っているためではないかと思う。 Keter についてはよく分かっていないのだが、Web アプリケーションのデプロイをサポートするものとのことで、 Yesod アプリのデプロイなどに使われているらしい。 ドキュメントによると Keter はログローテーションのAPIを持っているので、 Yesod 陣営にとっては monad-logger にローテーションを実装する必要性がないのではないだろうか。

まとめ

Katip はタイムスタンプや名前空間など、一般的なロガーライブラリが持っているような機能は十分備えている。 ログローテーションの仕組みはないが、これは logrotate コマンドなどを使えばなんとかなるので大きい問題ではないと思う。

なお Katip はこの記事で紹介した以外にも次のような機能がある。

続・Haskellの最近の例外ハンドリング

前回 の記事ではHaskellの例外ハンドリングには exceptions パッケージを使えばいいのではないかと書いた。

ところが今年の6月に safe-exceptions という exceptions を拡張したようなパッケージがさる FPComplete から 発表 された。

そこでこの記事では safe-exceptions について調べてみる。 おそらくほぼ FPComplete の発表の受け売りになってしまうので英語を読める人は原文を読む方がいいかもしれない。

さすが FPComplete だけあってこれは既に LTS Haskell に入っている。 この記事では lts-6.14 を用いる。

Haskellの例外のつらいところ

自分が認識している範囲ではHaskellの例外まわりは以下のところがつらい。

  1. 標準の例外系の関数が IO に特化されていて取り回しが悪い
  2. いかにも純粋そうな関数が例外を出したりスレッドセーフでなかったりする
  3. 例外型は何でも非同期例外として投げることができるし、キャッチ部で非同期例外かそうでないかの区別をつけられない

1.については前回の記事で紹介した exceptions パッケージによってかなり改善される。

2.は内部的にFFIでC関数を呼ぶライブラリを使うとたまにある。 まあ例外出すかやスレッドセーフかどうかはHaddockに大体書いてあるし、気になるのは好みの問題かもしれない。

3.が結構な問題である。 非同期例外というのは別のスレッドから来る例外で、 コード上ではtry節の外で発生する。 以下の someFunccatch はもちろん InternalException をキャッチするために書かれている。 しかし実際は ExternalException をキャッチしてしまうのである!

{-# LANGUAGE ScopedTypeVariables, DeriveDataTypeable #-}

module Main where

import Data.Typeable (Typeable)
import Control.Concurrent (forkIO, threadDelay)
import Control.Exception

data InternalException = InternalException String
                       deriving (Show, Typeable)
instance Exception InternalException

data ExternalException = ExternalException String
                       deriving (Show, Typeable)
instance Exception ExternalException

someFunc :: IO ()
someFunc = do
    (
        do
            threadDelay $ 1000 * 1000
            throw $ InternalException "error form inside!"
        ) `catch` (\(e::SomeException) -> do
            print e
        )
        
main :: IO ()
main = do
    threadId <- forkIO someFunc
    threadDelay $ 500 * 1000
    throwTo threadId $ ExternalException "error from outside!"
    threadDelay $ 100 * 1000
    return ()

-- > ExternalException "error from outside!"

この例では InternalExceptionExternalException で同期例外と非同期例外を型で区別できそうにも見える。 しかし throwTo はどんな例外でも投げることができるので InternalException も投げる可能性を排除できない。

すべての catch は非同期例外を受け取りうるし、キャッチした例外が同期か非同期か区別できないということを 念頭に置いて書かなければならない。 これは非常につらい。

safe-exceptions以前の対処

enclosed-exceptions パッケージというものがある。 enclosedの名前のとおり、try部の中の例外だけをキャッチする関数を提供する。

その仕組みは FPCompleteの記事 に書かれているが、 非同期処理をもって非同期例外を倒すという感じで面白い。

safe-exceptions

enclosed-exceptions は内部的に非同期処理を用いて非同期例外をうまく扱う。 しかし非同期処理を使うことのオーバーヘッドが生まれてしまった。

safe-exceptions は別のアプローチを用いる。 このライブラリは throwthrowTo をラップして同名で再エクスポートする。 これらは実際に例外を投げる「前」に例外を SyncExceptionWrapper もしくは AsyncExceptionWrapper で包んで投げる。 catch は同期例外と非同期例外を型で区別できないので、投げるときに区別できるようにしてしまうわけだ。 非同期処理を使っていないので非同期処理によるオーバーヘッドもない。

このライブラリは throwthrowTo のほかにも、 throwIOcatchcatchIO などの おなじみの関数を再実装しているので新たに使いかたを学ぶ必要はほとんどない。 しかも exceptions と同様にモナド変換子に対応している。

もし危険性を理解して非同期例外もキャッチしたいときには catchDeep が使える。

まとめ

safe-exceptions を使おう。

Haskellの最近の例外ハンドリング

どうもHaskellには標準のControl.Exceptionモジュールだけでなくmtlexceptionsexceptionalといった例外を扱うためのパッケージがあるらしいのだが、そのあたりのパッケージの選び方や使い方についてまとまった情報を見つけられなかった。 HaskellWiki例外のページも少々古いようで、deprecatedなものや統合される前のパッケージを書いていたりする。

調べた限り、mtlexceptionsが今の主流っぽい。 その2つの使い方をまとめる。

なおバージョンはlts-6.1を基準としている。

mtl

mtlパッケージのControl.Monad.ExceptモジュールはMonadErrorというモナドExceptTというモナド変換子を提供する。 以下のように使う。

import Control.Monad.Trans(lift)
import Control.Monad.Except(runExceptT, throwError, catchError)

main :: IO ()
main = do
    runExceptT $ do
        (do
                lift $ print "start"
                throwError "exception!"
                lift $ print "finish"
            ) `catchError` (\e -> do
                lift $ print "caught"
                lift $ print e
            )
    return ()

-- > "start"
-- > "caught"
-- > "exception!"

throwErrorcatchErrorMonadErrorのプリミティブで、 runExceptTExceptTをはがす役である。

catchErrorでキャッチしない場合はEitherとして取り出せる。

main = do
    ret <- runExceptT $ do
        lift $ print "start"
        throwError "exception!"
        return "ret value"
    print ret

-- > "start"
-- > Left "exception!"

あくまでthrowErrorをキャッチするものだということに注意:

main = do
    ret <- runExceptT $ do
        lift $ print "start"
        file <- lift $ readFile "nothing.txt"
        throwError "exception!"
        lift $ print file
        return "ret value"
    print ret

-- > "start"
-- > except-test-exe: nothing.txt: openFile: does not exist (No such file or directory)

実はIOMonadErrorインスタンスなので、IO中のIOExceptionをキャッチしたい場合はrunExceptTなしで] 以下のように書ける。

main = do
    (do
            print "start"
            file <- readFile "nothing.txt"
            print file
        ) `catchError` (\e -> do
            print "caught"
            print e
        )

-- > "start"
-- > "caught"
-- > nothing.txt: openFile: does not exist (No such file or directory)

ただし、IOMonadErrorとして使う場合はthrowErrorできるのはIOExceptionだけである。 以下のようなものはコンパイルエラーになってしまう。

main = do
    (do
            print "start"
            file <- readFile "nothing.txt"
            throwError "my error"          -- > Couldn't match type ‘GHC.IO.Exception.IOException’ with ‘[Char]’
            print file
        ) `catchError` (\e -> do
            print "caught"
            print e
        )

自作の別の例外を投げたいときはやはりrunExceptTを使う必要がある。

exceptions

exceptionsパッケージのControl.Monad.Catchモジュールは標準のControl.Exceptionに対してよりフレンドリーである。 Control.Exceptionの大部分をラップあるいは再エクスポートしており、Control.Exceptionをインポートしなくても Control.Exceptionと同じ名前の関数が同じように使える。

{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE ScopedTypeVariables #-}

import Data.Typeable
import Control.Monad.Catch

data MyException = MyException String
                 deriving(Show, Typeable)
instance Exception MyException

data MyAnotherException = MyAnotherException String
                  deriving(Show, Typeable)
instance Exception MyAnotherException

main = do
    (do
            print "start"
            throwM $ MyException "exception!"
            throwM $ MyAnotherException "another exception!"
            print "finish"
        ) `catch` (\(e::SomeException) -> do
            print "caught"
            print e
        )
-- > "start"
-- > "caught"
-- > MyException "exception!"

Control.Exceptionとの違いはthrowthrowMに変わるくらいである。 Control.ExceptionthrowモナドIOに限定されてしまうが、exceptionsthrowMではMonadThrowを実装しているモナドであればよい。 MonadThrowの実装が提供されているのはIOのほかにはmtlパッケージの各モナド(WriterTなど)がある。

まとめ

例外のためにモナドスタックを一段増やすのは例外の意義から言ってもやりすぎのような気がする。 IOがすでに失敗しうるという意味を含んでいるし。

個人的には実用上はexceptionsを使えばよいと思う。