このブログ記事では、Haskellの重要な型クラスであるExceptionを紹介します。

Exception
物事がうまくいかない場合、それを管理しなければなりません。例えば、div
関数は0で割ると動作しませんので、この場合に対処する方法が必要です。
以前はMaybe
を使ってこれを行いましたが、Exception
(例外)も使用できます。Exception
は以下のような多くのインスタンスを持つ型クラスです。
import Control.Exception -- インポート
-- 例
instance Exception SomeAsyncException
instance Exception IOException
instance Exception ArithException
instance Exception TypeError
--- その他多数
例外を投げるには、次のようにします。
divide = do
x <- getFloat
y <- getFloat
if y == 0 then throw DivideByZero else do return $ x / y
上記のコードは、ユーザーが提供したy
が0の場合、Control.Exception
モジュール内のDivideByZero
例外をthrow
を使って投げています。
また、独自の例外を定義してそれを投げることもできます。
data MyError = ErrorA deriving Show
instance Exception MyError
throw ErrorA -- => ***Exception: ErrorA
エラーをException
のインスタンスにするために関数を定義する必要はありませんが、Show
型クラスに属する必要があります。
捕捉
例外はユーザーに表示されるように捕捉される必要があります。表示には画面に出力することが含まれるため、例外は不純なコードまたは
IOモナド内でのみ捕捉されます。divide
の例外をmain
IOアクションで捕捉してみましょう。
-- IOアクション内で例外を捕捉する関数
catch :: Exception e => IO a -> (e -> IO a) -> IO a
main :: IO Float
main = do
catch divide (\e ->
do
let t = e :: ArithException
putStrLn "0では割れません。"
return 0
)
上記のcatch
関数は、例外の可能性があるIOアクション、例外のハンドラーを取り、同じ型のIOアクションを返します。ここでは、divide
に対して使用され、
DivideByZero
がArithException
データ型の値コンストラクタであるため、ArithException
に対するハンドラーを使用しています。ハンドラーはユーザーに
警告するために「0では割れません。」と表示し、0を返してdivide
と同じ型であるIO Float
となるようにします。
すべての例外はSomeException
であるため、全ての例外の捕捉としてSomeException
を使用することもできますが、すべての例外に対してハンドラーを異なる方法で定義する方が良いため、
これは必ずしも良い方法ではありません。複数の例外を捕捉する場合、代わりに次のようにできます。
f = <expr> `catch` \ (ex :: ArithException) -> handleArith ex
`catch` \ (ex :: IOException) -> handleIO ex
上記のコードは一見良さそうですが、実際にはhandleArith
ハンドラーからIOException
を捕捉する可能性があるため、これは悪い方法です。
望むのは、<expr>
からのみArithException
またはIOException
を捕捉することです。これを達成するために、catches
関数を使用できます。
-- 複数の例外を捕捉
catches :: Exception e => IO a -> [Handler (e -> IO a)] -> IO a
-- catchesで書き直し
f = <expr> `catches` [Handler (\ (ex :: ArithException) -> handleArith ex),
Handler (\ (ex :: IOException) -> handleIO ex)]
Try
例外を捕捉するもう一つの方法はtry
関数を使用することです。これはIOアクションを取り、その結果をLeft
に例外、
Right
に計算結果を含むEither
型のIOアクションを出力します。
-- try関数
try :: Exception e => IO a -> IO (Either e a)
main :: IO Float
main = do
result <- try divide :: IO (Either ArithException Float)
case result of
Left ex -> do
putStrLn "You divided by 0. "
return 0
Right val -> return val
try
はcatch
とほぼ同じですが、catch
と異なり、try
は非同期例外によって中断される可能性があります。
また、純粋な関数から例外を捕捉したい場合は、evaluate
を使用して純粋な関数の実行を強制する必要があります。
(これはcatch
も同様。)
-- 例外を投げる純粋なdivide関数
divide x y
| y == 0 = throw DivideByZero
| otherwise = x / y
-- 純粋関数の実行にevaluateを使用
main = do
result <- try (evaluate(divide 5 0)) :: IO (Either ArithException Float)
case result of
Left ex -> do
putStrLn "You divided by 0. "
Right val -> print val
try
、catch
、catches
の他にもtryJust
、catchJust
、finally
などの事前定義された関数があります。それについて興味があるなら、
ググってみてください!(プログラマーにとってググることは最も重要なスキルの一つです。)
例外を使用するタイミング
Exception
について多くを学びましたが、どのようにしてMaybe
、Either
、Exception
を使い分ければ良いのでしょうか?
最も簡単な答えは、「可能な限りException
は使わずMaybe
やEither
を使う」です。なぜなら、Exception
は本当は関数型の思想に反しているからです。
IO、スレッド、システムに関する場合はException
は避けられないので使わざるをえませんが、それ以外の場合は、Maybe
やEither
を使用するようにしてください。
クイズ
この記事では、学習した内容を確認するためのクイズを設けます。記事のメイン部分を読んだ後に、ぜひ自分で問題を解いてみることを強くお勧めします。各問題をクリックすると答えが表示されます。
リソース
- Philipp, Hagenlocher. 2020. Haskell for Imperative Programmers #27 - Exceptions. YouTube.