Миран Липовача - Изучай Haskell во имя добра!
Простейший способ выполнить действие, которое потенциально может вызвать исключение,– воспользоваться функцией 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
main = do
args <- getArgs
res <- try (mainAction args) :: IO (Either SomeException ())
case res of
Left e -> putStrLn "Ошибка"
Right () -> putStrLn "OK"
putStrLn "Конец программы"
Мы были вынуждены заменить тип исключения на SomeException и сделать сообщение об ошибке менее информативным, поскольку теперь неизвестно, исключение какого вида в данном случае произошло.
$ ./quotients a b
Ошибка
Конец программы
$ ./quotients
Ошибка
Конец программы
Понятно, что в общем случае обработка исключения должна зависеть от её типа. Предположим, что у нас имеется несколько обработчиков для исключений разных типов:
handleArith :: ArithException -> IO ()
handleArith _ = putStrLn "Деление на 0!"
handleArgs :: PatternMatchFail -> IO ()
handleArgs _ = putStrLn "Неверное число параметров командной строки!"
handleOthers :: SomeException -> IO ()
handleOthers e = putStrLn $ "Неизвестное исключение: " ++ show e
К сожалению, чтобы увидеть исключение от функции read, нужно воспользоваться наиболее общим типом SomeException.
Вместо того чтобы вручную вызывать функцию обработчика при анализе результата try, можно применить функцию catch, вот её тип:
ghci> :t catch
catch :: Exception e => IO a -> (e -> IO a) -> IO a
ПРИМЕЧАНИЕ. Модуль Prelude экспортирует старую версию функции catch, которая способна обрабатывать только исключения ввода-вывода. Чтобы использовать новый вариант её определения, необходимо использовать скрывающий импорт: import Prelude hiding (catch).
Функция catch принимает в качестве параметров действие и обработчик исключения: если при выполнении действия генерируется исключение, то вызывается его обработчик. Тип обработчика определяет, какие именно исключения будут обработаны. Рассмотрим примеры, в которых функция mainAction вызывается непосредственно в GHCi:
ghci> mainAction ["2","0"]
*** Exception: divide by zero
ghci> mainAction ["0","2"] `catch` handleArith
0
Деление на 0!
ghci> mainAction ["2","0"] `catch` handleArgs
*** Exception: divide by zero
ghci> mainAction ["2","0"] `catch` handleOthers
Неизвестное исключение: divide by zero
ghci> mainAction ["a", "b"] `catch` handleArgs
*** Exception: Prelude.read: no parse
ghci> mainAction ["a", "b"] `catch` handleOthers
Неизвестное исключение: Prelude.read: no parse
Если строка, выводимая GHCi, начинается с ***, то соответствующее исключение не было обработано. Обратите внимание на обычный для функции catch инфиксный способ вызова. Заметьте также, что обработчик handleOthers способен обработать любое исключение.
Вернёмся к основной программе. Нам хочется, чтобы возникшее исключение было обработано наиболее подходящим образом: если произошло деление на ноль, то следует выполнить handleArith, при неверном числе параметров командной строки – handleArgs, в остальных случаях – handleOthers. В этом нам поможет функция catches, посмотрим на её тип:
> :t catches
catches :: IO a -> [Handler a] -> IO a
Функция catches принимает в качестве параметров действие и список обработчиков (функций, которые упакованы конструктором данных Handler) и возвращает результат действия. Если в процессе выполнения происходит исключение, то вызывается первый из подходящих по типу исключения обработчиков (поэтому, в частности, обработчик handleOthers должен быть последним). Перепишем функцию main так, чтобы корректно обрабатывались все возможные исключительные ситуации:
main = do
args <- getArgs
mainAction args `catches`
[Handler handleArith,
Handler handleArgs,
Handler handleOthers]
putStrLn "Конец программы"
Посмотрим, как она теперь работает:
$ ./quotients 20 10
2
0
Конец программы
$ ./quotients
Неверное число параметров командной строки!
Конец программы
$ ./quotients 2 0
Деление на 0!
Конец программы
$ ./quotients a b
Неизвестное исключение: Prelude.read: no parse
Конец программы
В этом разделе мы разобрались с работой функций try, catch и catches, позволяющих обработать исключение, в том числе и возникшее в чистом коде. Заметьте ещё раз, что вся обработка выполнялась в рамках действий ввода-вывода. Посмотрим теперь, как работать с исключениями, которые возникают при выполнении операций ввода-вывода.
Обработка исключений ввода-вывода
Исключения ввода-вывода происходят, когда что-то пошло не так при взаимодействии с внешним миром в действии ввода-вывода, являющемся частью функции main. Например, мы пытаемся открыть файл, и тут оказывается, что он был удалён, или ещё что-нибудь в этом духе. Посмотрите на программу, открывающую файл, имя которого передаётся в командной строке, и говорящую нам, сколько строк содержится в файле:
import System.Environment
import System.IO
main = do
(fileName:_) <– getArgs
contents <– readFile fileName
putStrLn $ "В этом файле " ++ show (length (lines contents)) ++
" строк!"
Очень простая программа. Мы выполняем действие ввода-вывода getArgs и связываем первую строку в возвращённом списке с идентификатором fileName. Затем связываем имя contents с содержимым файла. Применяем функцию lines к contents, чтобы получить список строк, считаем их количество и передаём его функции show, чтобы получить строковое представление числа. Это работает – но что получится, если передать программе имя несуществующего файла?
Откройте для себя мир чтения на siteknig.com - месте, где каждая книга оживает прямо в браузере. Здесь вас уже ждёт произведение Миран Липовача - Изучай Haskell во имя добра!, относящееся к жанру Программирование. Никаких регистраций, никаких преград - только вы и история, доступная в полном формате. Наш литературный портал создан для тех, кто любит комфорт: хотите читать с телефона - пожалуйста; предпочитаете ноутбук - идеально! Все книги открываются моментально и представлены полностью, без сокращений и скрытых страниц. Каталог жанров поможет вам быстро найти что-то по настроению: увлекательный роман, динамичное фэнтези, глубокую классику или лёгкое чтение перед сном. Мы ежедневно расширяем библиотеку, добавляя новые произведения, чтобы вам всегда было что открыть "на потом". Сегодня на siteknig.com доступно более 200000 книг - и каждая готова стать вашей новой любимой. Просто выбирайте, открывайте и наслаждайтесь чтением там, где вам удобно.

