Haskell 例外処理 超入門

More than 1 year has passed since last update.

Haskellでは失敗を処理するのにモナドと例外の2つの方法があります。手っ取り早く失敗を処理することを目的として、それらの初歩を説明します。

シリーズの記事です。


  1. Haskell 超入門

  2. Haskell 代数的データ型 超入門

  3. Haskell アクション 超入門

  4. Haskell ラムダ 超入門

  5. Haskell アクションとラムダ 超入門

  6. Haskell IOモナド 超入門

  7. Haskell リストモナド 超入門

  8. Haskell Maybeモナド 超入門

  9. Haskell 状態系モナド 超入門

  10. Haskell モナド変換子 超入門

  11. Haskell 例外処理 超入門 ← この記事

  12. Haskell 構文解析 超入門

  13. 【予定】Haskell 継続モナド 超入門

  14. 【予定】Haskell 型クラス 超入門

  15. 【予定】Haskell モナドとゆかいな仲間たち

  16. 【予定】Haskell Freeモナド 超入門

  17. 【予定】Haskell Operationalモナド 超入門

  18. 【予定】Haskell Effモナド 超入門

  19. 【予定】Haskell アロー 超入門

練習の解答例は別記事に掲載します。


Eitherモナド


型表記

Either a b


成功時だけでなく、失敗時にも値が返せるモナドです。Maybeモナドの機能強化版のような失敗系モナドです。

Maybeモナドと比較します。型変数のabを配置位置から左右と呼んでいます。


失敗
成功

Maybe a
Nothing
Just a

Either a b
Left a
Right b

Rightが成功を表すのは、「正しい」という意味があるのに掛けているようです。


return

returnではRightが返されます。RightJustに対応していることを思い浮かべてください。

main = do

print (return 1 :: Either () Int)


実行結果

Right 1


型を指定するため型注釈が必要です。


either

Data.Eitherモジュールの関数です。



either :: (a -> c) -> (b -> c) -> Either a b -> c


LeftRightの値を処理する関数を渡してEitherを取り除きます。

import Data.Either

test = either (+ 1) (* 2)

main = do
print $ test $ Left 4
print $ test $ Right 4


実行結果

5

8


id

eitheridを組み合わせれば、LeftRightの違いを無視して値が取り出せます。

Either a babが同じ型のときに限ります。

import Data.Either

main = do
print $ either id id $ Left 4
print $ either id id $ Right 8


実行結果

4

8


脱出

MaybeモナドではNothingによりdoから脱出できるのと同じように、EitherモナドでもLeftdoから脱出できます。

test1 = do

a <- Right 1
Left () -- 脱出
return a

test2 = do
a <- Just 1
Nothing -- 脱出
return a

main = do
print test1
print test2


実行結果

Left ()

Nothing


エラーの理由

Eitherモナドの一般的な使い方です。

Nothingと違ってLeftには値が添えられます。これを使って脱出時に文字列を返すことができます。

import Control.Monad

test x = do
when (x < 5) $ Left $ show x ++ " < 5"
return $ x * 2

main = do
print $ test 4
print $ test 8


実行結果

Left "4 < 5"

Right 16

このLeftはエラーを表していると考えれば、添えられる文字列はエラーの理由となります。


値を返す

Leftは途中で値を返して抜けるのにも使用できます。

import Control.Monad

test x = either id id $ do
when (x < 5) $ Left $ x + 1
return $ x * 2

main = do
print $ test 4
print $ test 8


実行結果

5

16

これはエラー処理ではなく、次のような手続型のコードを模倣しています。


JavaScript

function test(x) {

if (x < 5) return x + 1;
return x * 2;
}

console.log(test(4));
console.log(test(8));



実行結果

5

16


練習

【問1】次のコードで脱出の理由(out of range, not xxx: 'X')を返すように修正してください。

import Data.Char

import Control.Monad

getch s n
| n < length s = Just $ s !! n
| otherwise = Nothing

test s = do
ch0 <- getch s 0
ch1 <- getch s 1
ch2 <- getch s 2
unless (isUpper ch0) Nothing
unless (isLower ch1) Nothing
unless (isDigit ch2) Nothing
return [ch0, ch1, ch2]

main = do
print $ test "Aa0"
print $ test "A"
print $ test "aa0"
print $ test "AA0"
print $ test "Aaa"

解答例

【問2】次のJavaScriptのコードを移植してください。Eitherモナドを使ってなるべく同じ構造にしてください。


JavaScript

function fizzBuzz(x) {

if (x % 3 == 0 && x % 5 == 0) return "FizzBuzz";
if (x % 3 == 0) return "Fizz";
if (x % 5 == 0) return "Buzz";
return x.toString();
}

for (var i = 1; i <= 15; ++i) {
console.log(fizzBuzz(i));
}


解答例


例外

実行時エラーは例外として伝達されます。IOアクションで入出力を行う際にも発生することがあります。

例外処理はIOモナドのブロックに対して行うため、例外が絡むとIOモナドに縛られてしまいます。IOモナドが絡まない処理ではEitherモナドを使うことが望ましいです(詳細は後述)。

【追記 2016.07.03】以下に書くのは古い方法になりつつあるようです。最近の動向は以下の記事を参照してください。


パターンマッチの失敗

実行時エラーでありがちなのはパターンマッチの失敗です。

foo 1 = "1"

main = do
print $ foo 0 -- エラー
print $ foo 1 -- 中断され到達しない
print "end"


実行結果(エラー)

xxx: Main.hs:1:1-11: Non-exhaustive patterns in function foo


例外が発生した時点でプログラムの実行が止まります。


catch

Control.Exceptionモジュールの関数です。



catch :: Exception e => IO a -> (e -> IO a) -> IO a


例外をキャッチして処理します。第2引数は例外ハンドラで、引数の型で例外の種類を指定する必要があります。

すべての例外をキャッチするにはSomeException型を指定します。

import Control.Exception           -- 追加

foo 1 = "1"

main = do
catch (do -- 使用箇所
print $ foo 0 -- 失敗
print $ foo 1 -- 実行されない
) $ \(SomeException e) -> -- 例外ハンドラ
print e -- 例外を表示
print "end" -- ハンドリング後に続行


実行結果

Main.hs:3:1-11: Non-exhaustive patterns in function foo

"end"


例外が発生した時点でハンドラに処理が移ります。ハンドリング対象のブロックは中断され戻りません。


演算子化

catchを演算子化して使う方法があります。他の言語(Javaなど)の見た目に近くなり括弧も減ります。

import Control.Exception

foo 1 = "1"

main = do
do -- 対象のブロック
print $ foo 0
print $ foo 1
`catch` \(SomeException e) -> -- catchを演算子化
print e
print "end"


実行結果

Main.hs:3:1-11: Non-exhaustive patterns in function foo

"end"



例外の型

例外の型が合わなければキャッチできません。

import Control.Exception

main = do
do
print $ 1 `div` 0 -- ゼロ除算
`catch` \(PatternMatchFail _) -> -- パターンマッチ失敗ではなくキャッチできない
print "PatternMatchFail"
print "end" -- 中断され到達しない


実行結果(エラー)

xxx: divide by zero



catches



catches :: IO a -> [Handler a] -> IO a


例外の型によってハンドラを振り分けます。リストに並べた順番で型がチェックされ、最初にマッチしたハンドラが実行されます。

import Control.Exception

main = do
do
print $ 1 `div` 0 -- ゼロ除算
`catches` -- 使用箇所
[ Handler $ \(PatternMatchFail _) ->
print "PatternMatchFail"
, Handler $ \(DivideByZero) -> -- マッチ
print "DivideByZero"
, Handler $ \(SomeException _) ->
print "SomeException"
]
print "end" -- ハンドリング後に続行


実行結果

"DivideByZero"

"end"

DivideByZeroは列挙型のデータ構築子で引数がありません。


練習

【問3】次のコードは例外が発生して途中で止まってしまいます。例外が発生したら元の文字列を表示して続行するように修正してください。

import Control.Monad

main =
forM_ ["1", "a", "3"] $ \s ->
print (read s :: Int)

具体的には以下のように出力してください。


実行結果

1

"a"
3

解答例


参照透過な関数

参照透過な関数(純粋関数)で発生した例外を扱う上で、いくつか注意点があります。


例外のすり抜け

catchの対象はIOアクションです。関数の戻り値をreturnでアクション化しようとしても、例外がキャッチできずにすり抜けてしまいます。

import Control.Exception

foo 1 = "1"

test = do
return $ foo 0 -- 失敗
`catch` \(SomeException _) -> -- すり抜ける
return "???" -- 実行されない

main = do
print =<< test


実行結果(エラー)

Main.hs:4:1-11: Non-exhaustive patterns in function foo


これは遅延評価に起因します。catchからはfoo 0という式(サンク)が入ったIOアクションが返され、そこから値を取り出すのはcatchの外で行われるためです。


evaluate



evaluate :: a -> IO a


関数をその場で評価します。遅延評価(非正格評価)に対して正格評価と呼びます。

catchと組み合わせることで例外のすり抜けを防止できます。

import Control.Exception

foo 1 = "1"

test = do
evaluate $ foo 0 -- 正格評価 → 失敗
`catch` \(SomeException _) -> -- キャッチされる
return "???" -- 実行される

main = do
print =<< test


実行結果

"???"



指針

IOモナドが絡まない処理では、例外は避けた方が無難です。評価が失敗する可能性のある関数は、MaybeモナドかEitherモナドを返すようにします。その方が記述も簡単になります。

foo 1 = Right "1"

foo _ = Left "???"

main = do
print $ foo 0


実行結果

Left "???"



練習

【問4】tryは例外をLeftに変換する関数です。次のコードはうまく動きませんが、tryの動作を確認できるように修正してください。

import Control.Exception

import Control.Monad

main = do
forM_ [0..3] $ \i -> do
a <- try $ return $ 6 `div` i
print (a :: Either SomeException Int)

解答例


モナド変換子

IO以外のモナドで失敗を扱うには、モナド変換子でEitherモナドを合成すれば例外が避けられます。

復習を兼ねてHaskell モナド変換子 超入門で出した例を書き替えます。MaybeモナドをEitherモナドにすることで、エラーの理由が返せるようになります。

次の手順でサンプルを書き換える過程を示します。


  • モナドなし


    • Stateモナド

    • Eitherモナド



  • モナド変換子で合成


モナドなし

getchで1文字ずつ取得して、get3で3文字分を返します。文字数が足らないと例外が発生します。

getch (x:xs) = (x, xs)

get3 xs0 =
let (x1, xs1) = getch xs0
(x2, xs2) = getch xs1
(x3, xs3) = getch xs2
in [x1, x2, x3]

main = do
print $ get3 "abcd" -- OK
print $ get3 "1234" -- OK
print $ get3 "a" -- NG


実行結果(エラー)

"abc"

"123"
xxx: Main.hs:1:1-22: Non-exhaustive patterns in function getch


Stateモナド

状態の受け渡しが冗長なので、Stateモナドで抽象化します。依然として例外は発生します。

import Control.Monad.State

getch = state getch where -- Stateモナドに関数を入れる
getch (x:xs) = (x, xs) -- 状態 -> (値, 状態)

get3 = evalState $ do
x1 <- getch
x2 <- getch
x3 <- getch
return [x1, x2, x3]

main = do
print $ get3 "abcd" -- OK
print $ get3 "1234" -- OK
print $ get3 "a" -- NG


実行結果

"abc"

"123"
xxx: Pattern match failure in do expression at Main.hs:4:5-10


Eitherモナド

最初のコードに戻って、例外を避けるためにEitherモナドで書き換えます。

getch (x:xs) = Right (x, xs)

getch _ = Left "too short"

get3 xs0 = do
(x1, xs1) <- getch xs0
(x2, xs2) <- getch xs1
(x3, xs3) <- getch xs2
return [x1, x2, x3]

main = do
print $ get3 "abcd" -- OK
print $ get3 "1234" -- OK
print $ get3 "a" -- NG


実行結果

Right "abc"

Right "123"
Left "too short"


モナド変換子で合成

StateTモナド変換子でEitherモナドと合成すれば、StateモナドとEitherモナドの両方の特徴が使えます。

import Control.Monad.State

getch = StateT getch where
getch (x:xs) = Right (x, xs)
getch _ = Left "too short"

get3 = evalStateT $ do
x1 <- getch
x2 <- getch
x3 <- getch
return [x1, x2, x3]

main = do
print $ get3 "abcd" -- OK
print $ get3 "1234" -- OK
print $ get3 "a" -- NG


実行結果

Right "abc"

Right "123"
Left "too short"

この応用で、簡単なものならモナド変換子で合成して処理できるでしょう。


練習

【問5】問1の解答をStateTモナド変換子を使って書き直してください。

具体的には次のコードが動くようにしてください。

test = evalStateT $ do

ch0 <- getch isUpper "upper"
ch1 <- getch isLower "lower"
ch2 <- getch isDigit "digit"
return [ch0, ch1, ch2]

解答例


参考

以下の記事を参考にさせていただきました。

各種言語における例外の概要が比較されている記事です。残念ながら個別の詳細記事は断念されていますが、Haskellについては次のように言及されています。


Haskellはいくつかの点でユニークな取り組みを行っています。例えば、Haskellは純粋な世界では例外機構を廃止しMaybe/Eitherを導入し、不純な世界では例外機構を導入しています。例外処理をある区分によって使い分けている意味で、Haskellの例外処理は先進的です。


evaluate絡みでハマった経験があります。