エラー処理を書いてはいけない

田中英行 tanaka.hideyuki@gmail.com
2011/12/08 @PFIセミナー

自己紹介

エラー処理を書いてはいけない

本日の概要

エラー処理を抽象化しようというお話です

エラーは処理しなければならない


 
 
エラー処理を書いてはいけない


エラー処理をしてはいけない

 
 
(※もちろん、エラーを処理する必要はあります)

??

つまりどうしろと?

エラー処理を書かずにエラーを処理しろということなんだよ!!!

cf. 書いてはいけないシリーズ

リソース開放を書いてはいけない
(認知度:高)

ロック処理を書いてはいけない
(認知度:中)

エラー処理を書いてはいけない
(認知度:?)


ときにに現代社会 ――


エラー処理をきっちり書く人は良いプログラマ

でも、そんなことはない

エラーにまつわるエトセトラ

エラー処理とはなんなのか?

エラーとは何か?

エラー処理に必要なこと

エラー通知あれこれ

intの返り値としてエラーを返す

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

Pros and Cons

Cons

なぜか goto 論争に発展

int foo()
{
int fd = 0, fe = 0, ff = 0;
fd = open(...);
if (fd < 0) goto finally;
fe = open(...);
if (fe < 0) goto finally;
ff = open(...);
if (ff < 0) goto finally;

...

finally:
if (fd > 0) close(fd);
if (fe > 0) close(fe);
if (ff > 0) close(ff);
return -1;
}

実際の例

エラー時にnullを返す

返り値をエラーとのタプルにする

返り値をエラーとの直和型にする

parseCXX :: String -> Either ParseError Syntax
parseCXX = ...

foo str =
case parseCXX str of
Left err -> error $ show err
Right ast ->
case syntaxAnal ast of
Left err -> error $ show err
Right code ->
...

Pros and Cons

オブジェクトをエラー状態にする

int main(int argc, char *argv)
{
ifstream ifs(argv[1]);
vector<uint8_t> buf(100);
ifs.read(&buf, 100);
if (!ifs) {
// error is occured!!
...
}
return 0;
}

Pros and Cons

初心者にはオススメしない… (例外が使えない環境での苦肉の策として用いられることが多いか)

例外を投げる

Pros and Cons

例外安全性

例外が起こっても、プログラムの実行がおかしくならないという性質

全人類が読むべき本:堂々の第一位

Exceptional C++ を何故読まねばならないのだろうか?

例外安全はむずかしいのです

Exceptionalで読むまで全く気づきもしませんでした。

Javaのthrows問題

template <class F>
void foo(F f)
{
f();
}

このようなものがJavaで書けるか? (throwsに何を書けばいいのか)

エラー処理いろいろありますが…

書かないエラー処理

書かないエラー処理とは

DRY


 
Don't

Repeat

Yourself

何故か抽象化されない

気を付けなければならない
⇔間違える

気を付けなければならないということは 必ずミスを犯すということ

バグを潰す
 ⇒ バグをジェノサイドする

大まかなアプローチ

エラー生成 <-> 利用する関数 <-> エラー処理 これらを分離する

エラー生成関数を利用する関数もまたエラー生成関数へ。

任意のエラー生成関数に対してエラーを処理できるバックエンドを たった一回実装。


 
 
 
 
そんな事出来るんですか?

できるんです。


 
 
 
 
そう、Haskellならね。

Haskellでのエラー処理

Haskellでのエラー通知手段あれこれ

実はどれも同じように扱える

どれをつかってもいいんです。 モナドのフレームワークを使えば。

異なるエラー型ごとに、床下の配線を(コンパイラが)組み替える

エラー処理を書いてみる : Maybe

extractJson json =
let mbusr = lookupJson json "user" in
case mbusr of
Nothing -> Nothing
Just usr ->
let mbmeta = lookupJson usr "meta" in
case mbmeta of
Nothing -> Nothing
Just meta ->
let mbname = lookupJson user "name" in
...

エラー処理を書いてみる : Maybeモナディック

extractJson json = do
user <- lookupJson json "user"
meta <- lookupJson json "meta"
name <- lookupJson json "name"
...

モナド使おう!

ここで重要なこと

前の例で重要なことは、 モナディックに記述してあるので、 Maybeについては一切言及していないということ


    ⇒


 何のエラー表現に対しても使える!

Monad as a Context

IOモナドの能力

IO モナドチートだった…

たまによくある インターフェース

IOとMaybeかぶってるよ!

getHttp :: String -> IO (Maybe Response)
getHttp url = ...

こういうのはIOだけで良い

getHttp :: String -> IO Response
getHttp url = ...

最新のインターフェース

最新の手法を用いた定義

getHttp :: (MonadIO m, Failure HttpError m) => String -> m Response

エラーを包括的に扱うフレームワーク

型は雄弁に語る

型を見れば何が起こるか大体わかる

getHttp :: (MonadIO m, Failure HttpError m) => String -> m Response

使用例

IOでも。

main :: IO 
main = do
resp <- getHttp "http://tanakh.jp"
...
`catch` ...

ErrorT IO でも。

main = do
resp <- runErrorT $ do
getHttp "http://tanakh.jp"
print resp -- Either HttpError Response

モナディックアプローチの良いところ

Alternativeとの組み合わせ

失敗した時の代替操作とか

foo :: IO String
foo = readFile "hoge" <|> readFile "hoge.exe"

失敗するまで繰り返すとか

bar :: IO [Int]
bar = many (readIO =<< getLine)

まとめ