単純に考えると、Haskell は関数型プログラミング言語のため、純粋な関数内では他のプログラミング言語における「関数の途中でエラー終了」はできません。
ただし、モナドを返す場合はそれに似た書き方が可能です。
(※実際には純粋な関数内でも例外を投げて計算を中断可能ですが、IO
モナド外で投げられた例外は捕まえられないため、純粋な関数内で例外を投げることは避けるべきです。)
参考「throw - Control.Exception」
参考「Catching Exceptions - Control.Exception」
0. まとめ
主に empty
の値を返すことでモナドの計算を中断することができます。
IO
モナドの場合は例外によって計算を中断できますが、やり方に注意が必要です (※後述) 。
それら以外の値でも中断できることがあります。
型クラス別:
-
Alternative
empty
-
MonadPlus
mzero
-
MonadFail
fail s
- その他
-
Either
型のLeft a
等
-
型別の例:
-
IO
empty
mzero
fail s
throwIO e
evaluate a
-
error s
,errorWithoutStackTrace s
-
Maybe
empty
mzero
fail s
Nothing
-
[]
empty
mzero
fail s
[]
-
Either
Left a
※ mzero
は歴史的な理由により残されていますが、mzero = empty
です。
※ Monoid
クラスの mempty
は意味が異なります。Alternative
クラスの empty
と同じ値の場合もありますが、必ずそうなっているわけではないため、モナドの計算を中断する目的では使用しません。
※ fail
関数は do
記法で自動的に利用される関数であり、fail s
を直接書くべきではありません。
※ IO
型では意味的には fail = throwIO . userError
です。
※戻り値が IO
型でない関数から投げられた例外は IO
モナド内であっても遅延評価され、意図したタイミングで計算を中断できないことがあります。evaluate
関数によって確実に中断することができます。
※ error
関数および errorWithoutStackTrace
関数は ErrorCall
例外を投げます。戻り値が IO
型の場合はそのまま使用でき、戻り値が IO
型でない場合は evaluate
関数と組み合わせる必要があります。
以下の関係が成り立ちます。
empty >>= f = empty
empty *> x = empty
fail s >>= f = fail s
fail s *> x = fail s
Left a >>= f = Left a
Left a *> x = Left a
evaluate
関数は pure
関数と同じような使い方をします。
ここでは説明のため型クラス制約を略しますが、型変数 f
の型は Applicative
クラスのインスタンスです。
pure :: a -> f a -- Applicative
evaluate :: a -> IO a -- IO
参考「empty - Control.Applicative」
参考「mzero - Control.Monad」
参考「fail - Control.Monad」
参考「3.14 Do Expressions - 3 Expressions」
参考「The fail method - Haskell/do notation - Wikibooks, open books for an open world」
参考「IO - System.IO」
参考「throwIO - Control.Exception」
参考「userError - System.IO.Error」
参考「catch - Control.Exception」
参考「evaluate - Control.Exception」
参考「error - Prelude」
参考「errorWithoutStackTrace - Prelude」
参考「ErrorCall - Control.Exception」
参考「Data.Either」
1. 使い方
import Control.Exception (catch, ErrorCall, evaluate, throwIO)
import System.IO (hPrint, stderr)
main :: IO ()
main = do
-- IO
do
throwIO $ userError "Error" :: IO ()
putStrLn "Foo" :: IO ()
`catch` \e -> do
hPrint stderr (e :: IOError)
do
errorWithoutStackTrace "Error" :: IO ()
putStrLn "Foo" :: IO ()
`catch` \e -> do
hPrint stderr (e :: ErrorCall)
do
evaluate (errorWithoutStackTrace "Error" :: ()) :: IO ()
putStrLn "Foo" :: IO ()
`catch` \e -> do
hPrint stderr (e :: ErrorCall)
-- Maybe
print $ do
Nothing :: Maybe ()
Just 23 :: Maybe Int
-- []
print $ do
[] :: [()]
[23, 42] :: [Int]
-- Either
print $ do
Left "Error" :: Either String ()
Right 23 :: Either String Int
user error (Error)
Error
Error
Nothing
[]
Left "Error"
import Control.Exception (ArithException, catch, ErrorCall, evaluate, throwIO)
import System.IO (hPrint, stderr)
main :: IO ()
main = do
-- IO
do
x :: Int <- throwIO $ userError "Error" :: IO Int
putStrLn "Foo" :: IO ()
print x :: IO ()
`catch` \e -> do
hPrint stderr (e :: IOError)
do
x :: Int <- evaluate (23 `div` 0 :: Int) :: IO Int
putStrLn "Foo" :: IO ()
print x :: IO ()
`catch` \e -> do
hPrint stderr (e :: ArithException)
do
x :: Int <- errorWithoutStackTrace "Error" :: IO Int
putStrLn "Foo" :: IO ()
print x :: IO ()
`catch` \e -> do
hPrint stderr (e :: ErrorCall)
do
x :: Int <- evaluate (errorWithoutStackTrace "Error" :: Int) :: IO Int
putStrLn "Foo" :: IO ()
print x :: IO ()
`catch` \e -> do
hPrint stderr (e :: ErrorCall)
-- Maybe
print $ do
x :: Int <- Nothing :: Maybe Int
Just x :: Maybe Int
-- []
print $ do
x :: Int <- [] :: [Int]
[x, 42] :: [Int]
-- Either
print $ do
x :: Int <- Left "Error" :: Either String Int
Right x :: Either String Int
user error (Error)
divide by zero
Error
Error
Nothing
[]
Left "Error"
2. 仕組み
2.1. Alternative
: empty
インスタンス Alternative f
に関して、empty :: f a
の値は a
型の値を持たないため、a -> b
型や a -> f b
型の関数を適用すると empty :: f b
の値になります。
Alternative
クラスのインスタンスに関して、以下の関係が成り立ちます。
f <$> empty = empty
f <*> empty = empty
empty <*> x = empty
empty $> x = empty
empty *> x = empty
Alternative
クラスと Monad
クラスの両方のインスタンスの場合、以下の関係が成り立ちます。
empty >>= f = empty
do
x <- empty
f x
= empty
do
empty
x
= empty
2.2. MonadPlus
: mzero
MonadPlus
クラスは Alternative
クラスと Monad
クラスを継承するクラスです。
歴史的な理由により残されていますが、Alternative
クラスと意味は同じです。
2.3. MonadFail
: fail s
MonadFail
クラスの fail
関数は、do
記法の <-
で左側のパターンが一致しないとき、自動的に利用されます。
z = do
C y <- x
f y
z = x >>= g
where
g (C y) = f y
g _ = fail "Pattern match failure"
fail s
は以下の条件を満たすよう定義されます。
fail s >>= f = fail s
MonadFail
クラスと Alternative
クラスの両方のインスタンスの場合、以下のように定義されることがあります。
fail _ = empty
MonadFail
クラスは例外を扱うためのものではありませんが、IO
型を含む MonadIO
クラスのインスタンスでは fail s
は例外を投げます。
IO
型での fail
関数の意味は以下のようになっています。
fail :: String -> IO a
fail = throwIO . userError
2.4. 戻り値が IO
型でない関数から投げられた例外を IO
モナド内で捕まえられるようにする
Haskell では、IO
モナド内で投げられた例外は捕まえることができますが、IO
モナド外で投げられた例外は捕まえることができません。
戻り値が IO
型でない関数から投げられた例外は IO
モナド内であっても遅延評価され、意図したタイミングで計算を中断できないことがあります。
import Control.Exception (ArithException, catch)
import System.IO (hPrint, stderr)
main :: IO ()
main = do
let x = 23 `div` 0 :: Int
putStrLn "Foo" :: IO ()
print x :: IO ()
`catch` \e -> do
hPrint stderr (e :: ArithException)
Foo
divide by zero
evaluate
関数で IO
モナドでない値を IO
モナドにすると、確実に中断することができます。
import Control.Exception (ArithException, catch, evaluate)
import System.IO (hPrint, stderr)
main :: IO ()
main = do
x :: Int <- evaluate (23 `div` 0 :: Int) :: IO Int
putStrLn "Foo" :: IO ()
print x :: IO ()
`catch` \e -> do
hPrint stderr (e :: ArithException)
divide by zero
2.5. error
関数および errorWithoutStackTrace
関数
error
関数および errorWithoutStackTrace
関数は ErrorCall
例外を投げます。
IO
モナド内で呼ぶ場合、error
関数および errorWithoutStackTrace
関数の戻り値が IO
型かによって挙動が異なるため注意が必要です。
2.5.1. 戻り値が IO
型の場合
error
関数および errorWithoutStackTrace
関数の戻り値が IO
型の場合、そのまま使用して意図したタイミングで計算を中断できます。
import Control.Exception (catch, ErrorCall)
import System.IO (hPrint, stderr)
main :: IO ()
main = do
errorWithoutStackTrace "Error" :: IO ()
putStrLn "Foo" :: IO ()
`catch` \e -> do
hPrint stderr (e :: ErrorCall)
Error
2.5.2. 戻り値が IO
型以外の場合
error
関数および errorWithoutStackTrace
関数の戻り値が IO
型以外の場合、遅延評価によって意図したタイミングで計算を中断できなかったり、そもそも評価されずに中断されないことがあります。
import Control.Exception (catch, ErrorCall)
import System.IO (hPrint, stderr)
main :: IO ()
main = do
let x = errorWithoutStackTrace "Error" :: Int
putStrLn "Foo" :: IO ()
print x :: IO ()
`catch` \e -> do
hPrint stderr (e :: ErrorCall)
Foo
Error
import Control.Exception (catch, ErrorCall)
import System.IO (hPrint, stderr)
main :: IO ()
main = do
pure (errorWithoutStackTrace "Error" :: ()) :: IO ()
putStrLn "Foo" :: IO ()
`catch` \e -> do
hPrint stderr (e :: ErrorCall)
Foo
evaluate
関数を用いることで、確実に中断することができます。
import Control.Exception (catch, ErrorCall, evaluate)
import System.IO (hPrint, stderr)
main :: IO ()
main = do
x :: Int <- evaluate (errorWithoutStackTrace "Error" :: Int) :: IO Int
putStrLn "Foo" :: IO ()
print x :: IO ()
`catch` \e -> do
hPrint stderr (e :: ErrorCall)
Error
import Control.Exception (catch, ErrorCall, evaluate)
import System.IO (hPrint, stderr)
main :: IO ()
main = do
evaluate (errorWithoutStackTrace "Error" :: ()) :: IO ()
putStrLn "Foo" :: IO ()
`catch` \e -> do
hPrint stderr (e :: ErrorCall)
Error