В стандарте языка Haskell 98 года присутствует механизм обработки исключений ввода-вывода, который в настоящее время считается устаревшим. Согласно современному подходу все исключения, возникшие как при выполнении чистого кода, так и при осуществлении ввода-вывода, должны обрабатываться единообразно. Этой цели служит единая иерархия типов исключений из модуля Control.Exception
, в которую легко можно включать собственные типы исключений. Любой тип исключения должен реализовывать экземпляр класса типов Exception
. В модуле Control.Exception
объявлено несколько конкретных типов исключений, среди которых IOException
(исключения ввода-вывода), ArithException
(арифметические ошибки, например, деление на ноль), ErrorCall
(вызов функции error
), PatternMatchFail
(не удалось выбрать подходящий образец в определении функции) и другие.
Простейший способ выполнить действие, которое потенциально может вызвать исключение,– воспользоваться функцией try
:
try :: Exception e => IO a -> IO (Either e a)
Функция try
пытается выполнить переданное ей действие ввода-вывода и возвращает либо Right
либо Left
, например:
ghci> try (print $ 5 `div` 2) :: IO (Either ArithException ())
2
Right ()
ghci> try (print $ 5 `div` 0) :: IO (Either ArithException ())
Left divide by zero
Обратите внимание, что в данном случае потребовалось явно указать тип выражения, поскольку для вывода типа информации недостаточно. Помимо прочего, указание типа исключения позволяет обрабатывать не все исключения, а только некоторые. В следующем примере исключение функцией try
обнаружено не будет:
> try (print $ 5 `div` 0) :: IO (Either IOException ())
*** Exception: divide by zero
Указание типа SomeException
позволяет обнаружить любое исключение:
ghci> try (print $ 5 `div` 0) :: IO (Either SomeException ())
Left divide by zero
Попробуем написать программу, которая принимает два числа в виде параметров командной строки, делит первое число на второе и наоборот и выводит результаты. Нашей первой целью будет корректная обработка ошибки деления на ноль.
import Control.Exception
import System.Environment
printQuotients :: Integer -> Integer -> IO ()
printQuotients a b = do
print $ a `div` b
print $ b `div` a
params :: [String] -> (Integer, Integer)
params [a,b] = (read a, read b)
main = do
args <- getArgs
let (a, b) = params args
res <- try (printQuotients a b) :: IO (Either ArithException ())
case res of
Left e -> putStrLn "Деление на 0!"
Right () -> putStrLn "OK"
putStrLn "Конец программы"
Погоняем программу на различных значениях:
$ ./quotients 20 7
2
0
OK
Конец программы
$ ./quotients 0 7
0
Деление на 0!
Конец программы
$ ./quotients 7 0
Деление на 0!
Конец программы
Понятно, что пока эта программа неустойчива к другим видам ошибок. В частности, мы можем «забыть» передать параметры командной строки или передать их не в том количестве:
$ ./quotients
quotients: quotients.hs:10:1-31: Non-exhaustive patterns in function params
$ ./quotients 2 3 4
quotients: quotients.hs:10:1-31: Non-exhaustive patterns in function params
Это исключение генерируется при вызове функции params
, если переданный ей список оказывается не двухэлементным. Можно также указать нечисловые параметры:
$ ./quotients a b
quotients: Prelude.read: no parse
Исключение здесь генерируется функцией read
, которая не в состоянии преобразовать переданный ей параметр к числовому типу.
Чтобы справиться с любыми возможными исключениями, выделим тело программы в отдельную функцию, оставив в функции main
получение параметров командной строки и обработку исключений:
mainAction :: [String] -> IO ()
mainAction args = do
let (a, b) = params args
printQuotients a b