Beautiful Error Handling

田中英行 <tanakh@preferred.jp>

2012年夏のプログラミング・シンポジウム

自己紹介

概要

エラー処理に美しさを!

背景

エラー処理の例(1)

int foo(...)
{
  int fd = open(...);
  if (fd < 0) return -1; // <- error handling
  ...
}

エラー処理の例(2)

int foo(...)
{
  int fd = open(...);
  if (fd < 0) return -1; // <- error handling
  int fe = open(...);
  if (fe < 0) {          // <- error handling
    close(fd);           // <- error handling
    return -1;           // <- error handling
  }                      // <- error handling
}

エラー処理の例(3)

int foo(...)
{
  int fd = open(...);
  if (fd < 0) return -1; // <- error handling
  int fe = open(...);
  if (fe < 0) {          // <- error handling
    close(fd);           // <- error handling
    return -1;           // <- error handling
  }                      // <- error handling
  int ff = open(...);
  if (ff < 0) {          // <- error handling
    close(fe);           // <- error handling
    close(fd);           // <- error handling
    return -1;           // <- error handling
  }                      // <- error handling
  ...
}

一方…

そういう背景からか、

という風潮も

なぜエラー処理に美しさが必要なのか?

所感

美しいプログラムは

つまり

(あくまでテクニカルに)

個人的な思惑

何が必要なのか?

エラー処理の抽象化が必要である

という理想の世界

エラー処理の抽象化

そもそもエラー処理とは

プログラムの実行中に発生するエラーに対して、 正しく回復処理・あるいは報告を行い、 何事もなかったかのように動き続ける堅牢なソフトウェアを書くための処理

様々なエラー

エラー通知の手段の一例

どれを使うべきか?

実際のところ一長一短

言語・標準ライブラリのデザインの問題

どのエラー通知を使うかは、言語デザインにも関わる

例外への批判

エラー処理が言語の設計にまで影響を及ぼす理由

複数のエラーを組み合わせて使うことが難しい

{
  try {
    Hoge h = new Hoge(foo);
    Moge i = h.find(x);
    if (i == null) {
        ...
    }
  }
  catch(HogeException e) {
    ...
  }
}

使い分けたい時もある

そもそも、何をエラーとして扱うかという話でもある

エラー通知とエラーハンドリングの切り離し

そのために必要なもの

そんなのができるのか・使えるのか?

メモリ管理との対比

Haskellでのアプローチ

エラーの抽象化に望まれること

Composable

プログラムを組み合わせ可能であるということ

正しいとは(ここでは)

これを組み合わせたプログラムにおいても保持する

例外

cf. 例外は抽象化の一例ではあるが、Composableではない

class Hoge {
  public void foo() {
    try {
      DB db = new DB(...);
      try {
        if (db.getRow(...)) {
          ...
        }
      }
      catch(...) {
        ...
      }
    }
    catch(...) {
      db.release();
    }
  }
}

Haskellでは

モナドを用いるアプローチ

Error モナド (in mtl)

class (Monad m) => MonadError e m | m -> e where
    -- | Is used within a monadic computation to begin exception processing.
    throwError :: e -> m a

    {- |
    A handler function to handle previous errors and return to normal execution.
    A common idiom is:

    > do { action1; action2; action3 } `catchError` handler

    where the @action@ functions can call 'throwError'.
    Note that @handler@ and the do-block must have the same return type.
    -}
    catchError :: m a -> (e -> m a) -> m a

エラーの送信(throwError)、エラーの受信(catchError)

Error モナド

IO例外を扱えるようにする

instance MonadError IOException IO where
    throwError = ioError
    catchError = catch

Eitherを扱えるようにする

instance Error e => MonadError e (Either e) where
    throwError             = Left
    Left  l `catchError` h = h l
    Right r `catchError` _ = Right r

instance (Monad m, Error e) => MonadError e (ErrorT e m) where
    throwError = ErrorT.throwError
    catchError = ErrorT.catchError

エラーハンドラの抽象化へ

よくあるパターンを抽象化できるようになる

これらを組み合わせて、

抽象化:エラー無視

ign :: MonadError e m => m () -> m ()
ign m = m `catchError` (\e -> return ())

受け取ったエラーを無視するだけのコード

抽象化:n回試行

tryN :: MonadError e m => Int -> m a -> m a
tryN n m = go n where
  go 1 = m
  go i = m `catchError` (\e -> go (i-1))

失敗したらカウンタを減らして再度実行

抽象化:aが失敗したらbを実行

or :: MonadError e m => m a -> m a -> m a
or a b = do
  a `catchError` (\_ -> b)

エラーハンドラでbを実行する

組み立てる

main = ign $ tryN 10 $ do
  download "http://xxx/aznn.png" `or`
  download "http://xxx/prpr.png"

あんなコードやこんなコードも自由自在!

技術的な課題

Composableであっても(正しくても)、 記述力が十分とは限らない。

monad-control

これに対する解答が最近ようやく確立

class MonadTrans t => MonadTransControl t where
  data StT t :: * -> *Source

  liftWith :: Monad m => (Run t -> m a) -> t m aSource

  -- liftWith is similar to lift in that it lifts a computation from the argument monad to the constructed monad.
  -- Instances should satisfy similar laws as the MonadTrans laws:

  -- restoreT :: Monad m => m (StT t a) -> t m a

type Run t = forall n b. Monad n => t n b -> n (StT t b)

ともかく、それなりに課題もある

別のアプローチ

まとめ

まとめ




Thank you for listening!