1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[Haskell] モナドの計算を中断する

Last updated at Posted at 2023-10-23

単純に考えると、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 クラスと意味は同じです。

参考「MonadPlus - Control.Monad

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

参考「MonadFail - Control.Monad

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
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?